最近看到了一些关于GCD的一些问题,看了下不是很清楚,发现这些问题还是很有意思的, 所以决定研究下GCD的源码,过程中还是找到了很多神奇的地方,作为学习的记录,于是写下了这个文章作为学习的笔记。。。
看的时候是最新的libdispatch-84.5.tar.gz
的版本,这个版本跟之前版本不太一样,不过总体上没有太大的改变。。。
1. dispatch_async
函数如何实现,分发到主队列和全局队列有什么区别,一定会新建线程执行任务么?
2. dispatch_sync
函数如何实现,为什么说 GCD 死锁是队列导致的而不是线程,死锁不是操作系统的概念么?
3. 信号量是如何实现的,有哪些使用场景?
4. dispatch_group
的等待与通知,dispatch_once
如何实现?
5. dispatch_source
用来做定时器如何实现,有什么优点和用途?
6. dispatch_suspend
和 dispatch_resume
如何实现,队列的的暂停和计时器的暂停有区别么?
打开源码,我们能看到大概的结构,先来看下提供给我们的接口:
接下来,分析下开发常用到一些函数。
dispatch_once
1 | void dispatch_once(dispatch_once_t *val, void (^block)(void)) { |
1 | void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)) { |
这个函数官方文档的是这样描述的, “Executes a block object once and only once for the lifetime of an application.” 就是说在程序的生命周期内block只执行一次.
dispatch_once
函数内部调用了 dispatch_once_f
, 在dispatch_once_f
中发现了几个奇怪的宏.
dispatch_atomic_cmpxchg
:#define dispatch_atomic_cmpxchg(p, o, n) __sync_bool_compare_and_swap((p), (o), (n))
…what ??__sync_bool_compare_and_swap
这是个什么玩意??查了下发现这是一种原子操作的一种技术CAS, CAS可以用于实现无锁数据结构, 这里就不展开了dispatch_atomic_barrier
:
1 |
|
这个宏定义在不同的系统架构上有不同的实现, 其实就是一种原子操作
_dispatch_hardware_pause
:#define _dispatch_hardware_pause() asm("pause")
, pause是一个汇编指令
函数在执行的时候,传进来一个类型为dispatch_once_t
的参数,dispatch_once_t
这个类型其实就是long
类型, * val如果没有初始化则值为0, 当走到if的时候, 与0比较相等,dispatch_atomic_cmpxchg
返回了YES, 就会执行func函数,其实就是外面传进来的block, 并把*val 置成1, 如果第二次再走这个函数, dispatch_atomic_cmpxchg
则会返回NO, 就只会走else后面的语句, block就不会再执行了, 所以这就能保证dispatch_once
函数只能执行一次的原因,其核心就是__sync_bool_compare_and_swap((p),(o),(n))
。
PS: 平时我们在开发时,经常会用这个函数来写单例,
1 | + (id)sharedInstance { |
假设我们需要把这个单例释放,我们可以把static Class *_sharedInstance = nil;
这一句放到函数外边, 外面直接将_sharedInstance置空即可,但是如果需要重新创建单例的话, 则还需要把onceToken
置为0, 不置为0的话就不会走block了。
dispatch_async
1 | void dispatch_async(dispatch_queue_t dq, void (^work)(void)) { |
“Submits a block for asynchronous execution on a dispatch queue and returns immediately.” 在一个队列提交一个异步执行的block,并立刻返回.
dispatch_async
内部执行了dispatch_async_f
,把block转成了function执行~~在这里我们发现了 fastpath
这个神奇的东西, 点进去这个宏。。发现是一个__builtin_expect
函数,这个可以用来优化执行速度的
- fastpath表示条件更可能成立
- slowpath表示条件更不可能成立
1 | #define fastpath(x) ((typeof(x))__builtin_expect((long)(x), ~0l)) |
先从_dispatch_continuation_alloc_cacheonly
取出是否有cache的dc;若有dc存在,那么给dc赋值,然后push到dq中去;假设没有cache,那么进入到_dispatch_async_f_slow
创建一个dispatch_continuation
对象(从堆中获取)来存放function,然后push到队列后面, 我们先来看下
1 | static inline dispatch_continuation_t _dispatch_continuation_alloc_cacheonly(void) { |
这里我们用到了TSD(Thread-Specific Data) 也就是线程私有数据。这里不展开。如果能取到dc, 则为它设置线程私有数据并返回.
dc->do_vtable = (void *)DISPATCH_OBJ_ASYNC_BIT
GCD队列的异步就是通过DISPATCH_OBJ_ASYNC_BIT
这个bit标识来实现的,全部的标识有如下几种:
1 |
dispatch_sync
1 | void dispatch_sync(dispatch_queue_t dq, void (^work)(void)) { |
“Submits a block object for execution on a dispatch queue and waits until that block completes.” 这个函数是在一个队列提交一个执行的block,并需要等待block必须执行完之后函数才返回.
发现dispatch_sync
无论走哪个分支,最后走的都是dispatch_sync_f
,通过_dispatch_Block_copy
或者Block_basic
来实现block到function的转换.
如果是串行队列(dq_width==1)的话必须要等待前面的任务执行完成之后才能执行该任务,然后调用dispatch_barrier_sync_f
,barrier的实现是依靠信号量机制来同步任务的执行.
1 | void dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,dispatch_function_t func) { |
如果当前是处于队列尾部,队列不为暂停状态,且使用_dispatch_queue_trylock检查队列则会执行_dispatch_barrier_sync_f_slow
函数
1 | static void _dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) { |
在函数中,使用_dispatch_queue_push
将我们的block压入main queue
的FIFO队列中,然后等待信号量,ready后被唤醒. 假如我们调用的dispatch_sync
函数一直不返回,而且main queue
被阻塞,而我们的block又需要等待main queue来执行它, 就会出现死锁. 所以 GCD 死锁是队列导致的而不是线程.
dispatch_after
1 | void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t work) { |
“Enqueue a block for execution at the specified time.” 延时提交用于执行的block.
同上内部会走dispatch_after_f
这个函数,实现将block跟function之间的转换, 如果外面传进来的时间为DISPATCH_TIME_FOREVER
,则直接返回, when
为0, 则执行dispatch_async_f
,内部是依赖dispatch_source
定时器来实现延迟执行, 如果没有使用到dispatch source
, function则会被优化。
dispatch_ get_ global_queue
1 | dispatch_queue_t dispatch_get_global_queue(long priority, unsigned long flags) { |
“Returns a system-defined global concurrent queue with the specified quality of service class.” 返回一个系统定义的全局并发队列, 并可以指定执行的服务质量
我们会看到有个DISPATCH_QUEUE_OVERCOMMIT
这么个玩意。。点进去看。发现官方是这么描述的, 首先它是一个枚举, 系统会开启一个新的线程去执行block, 无论此时计算机有多忙~ OVERCOMMIT
用来控制线程数能不能超越物理内核数
1 | /*! |
通过此方法_dispatch_get_root_queue
获取对应的一个全局队列。有这三种优先级以及是否是OVERCOMMIT
1 | enum { |
接下来我们来看一个图, 图中描述了线程和队列的关系
每一个_dispatch_root_queue
包含一个线程池_dispatch_root_queue_context
4-9是全局队列,1是主队列,2是一个管理队列,用来管理 GCD内部任务, 3暂时没使用. 队列 的 dq_width
被设置为 UINT32_MAX,表示这些队列不限制并发数.
1 | static struct dispatch_queue_s _dispatch_root_queues[] = { |
1 |
|
dispatch_root_queue_context_s
这个结构体就是队列对应的线程实现, 一个context就是一个线程池, 最大线程数为255
dispatch_ get_current _queue
1 | dispatch_queue_t dispatch_get_current_queue(void) { |
我们获取当前运行队列的时候是通过TSD(Thread-Specific-Data)来获取的, 如果没有当前队列, 则会通过_dispatch_get_root_queue
这个方法获取取一个优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT
的队列
dispatch _get _main _queue
1 | dispatch_queue_t dispatch_get_main_queue(void) { |
获取主队列, dq_width
等于1表示是一个串行队列, 支持overcommit, 且优先级为Default
讲了这么多GCD的源码, 现在来说一下会常用到的API:
dispatch_source
我们常用来实现定时器, 但是它跟NSTimer
有什么区别呢?
首相NSTimer
依赖于runloop, 由于runloop需要处理很多任务,导致NSTimer的精度降低. 如果忘了停止, 还会造成循环引用. dispatch_source_t
是由系统触发,所以精度很高, 下面是通过dispatch_source_t
创建一个计时器.
我们可以通过dispatch_source_t dispatch_source_create(dispatch_source_type_t type,uintptr_t handle,unsigned long mask,dispatch_queue_t _Nullable queue);
创建一个timer, 第一个参数是设置源的类型, 第二个参数可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID, 第三个参数可以理解为描述, 提供更详细的描述, 让它知道具体要监听啥, 第四个参数就是一个队列, 用来处理所有的响应.
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
取消dispatch source
是一个异步操作,即虽然在调用了dispatch_source_cancel
函数之后,dispatch source
不能再接收到任何事件,但它还可以继续处理在队列中的事件,直到在队列中的最后一个事件被执行完成后,dispatch source
才会执行cancellation handler句柄. 一般情况下是取消了dispatch source后,立即释放掉该对象.
dispatch _suspend / dispatch _resume
dispatch_suspend
可以挂起我们正在执行的队列, 追加到Dispatch Queue
中但尚未执行的处理在此之后停止执行, 需要注意的地方是, 这个方法并不会立即暂停正在运行的block,而是在当前block执行完成后,暂停后续的block执行.
dispatch _resume
则是唤醒已经挂起的que
dispatch_apply
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t))
这个方法提交一个block块到一个分发的队里,以供多次调用.第一个参数是指需要执行的次数, 第二个参数是执行的队列(假设队列是并发队列,则会并发执行block任务,GCD会帮我们管理并发, 不会造成线程爆炸), 第三个参数则是需要执行的block.
dispatch_apply
是一个同步调用, 需要block任务全部执行完后才返回.
dispatch_benchmark
uint64_t dispatch_benchmark(size_t count, void (^block)(void));
这个函数可以帮助我们测量计算n次block任务的时间, 也就是说可以直接返回一个时间, 相比于其他的CFAbsoluteTimeGetCurrent
相减, 方便很多.
dispatch_semaphore
这个也就是我们经常说的信号量了,信号量其实就是一个资源计数器,对信号量有两个操作来达到互斥, 分别是P和V操作, 设信号量值为1,当一个进程1运行时, 使用资源, 进行其进行P操作, 对信号量值减1, 也就是资源数少了1个. 这时信号量值为0, 系统中规定当信号量值为0时, 必须等待, 信号量值不为零才能继续操作. 这时如果进程2想要运行, 那么也必须进行P操作, 但是此时信号量为0, 所以无法减1, 即不能P操作, 也就阻塞. 这样就达到了进程1排他访问. 当进程1运行结束后, 释放资源, 进行V操作, 资源数重新加1, 这是信号量的值变为1. 这时进程2发现资源数不为0, 信号量能进行P操作了, 立即执行P操作. 信号量值又变为0. 进程2达到了排他访问.
所以我们可以通过信号量来达到线程同步的问题.
我们可以通过dispatch_semaphore_t dispatch_semaphore_create(long value)
来创建一个信号量
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
等待信号
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
使用这个函数来发送一个信号
dispatch _group _enter / dispatch _group _leave
dispatch_group_enter
: 告诉group, 下面的任务马上要放到group中执行了.
dispatch_group_leave
: 告诉group, 任务执行完了, 该任务要从group中移除了.
dispatch_group_wait
: 会阻塞当前线程(所以不能放在主线程调用)一直等待; 当group上任务完成,或者等待时间超过设置的超时时间会结束等待.
需要注意的是前两个函数必须成对使用, 否则则会永远等待.
dispatch_group_notify
通常我们应该使用这个, 不会阻塞当前线程,马上返回.
总结
由于工作比较忙, 也都是在空闲时间看的源码, 陆陆续续写了一个星期, 虽然有些地方理解的不是很好, 但是收获却不少, 作为学习的笔记记录了下来.
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/7563b745.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!