GCD系列 文章之线程同步与组,建议顺序为:概念,原理,之后在阅读本文
背景
源码及版本号。所有源码均在苹果开源官网下可下载
源码 | 版本
-|-
libdispatch | 1008.250.7
libpthread | 330.250.2
xnu | 6153.11.26
barrier
在就讲过之前的sync
和async
之后,barrier
函数就相对好理解了
sync
dispatch_barrier_sync
和dispatch_sync
组合串行队列的流程相似,由于同步操作,当前面的任务执行时,后面任务会进入wait
流程,关于等待与唤醒的方式,在前一篇已经有过说明。
async
dispatch_barrier_async
和dispatch_async
有所不同,在配合并行队列的情况下:经由_dispatch_lane_concurrent_push
会进入到
_dispatch_lane_push
(回忆上篇异步并行的流程)。
1 |
|
这里最终会调用到dx_wakeup
即:_dispatch_lane_wakeup。
这个函数内部会调用_dispatch_queue_wakeup
。
1 | void |
_dispatch_queue_push_queue
会进一步调用_dispatch_root_queue_push
在前一篇已经分析过流程,但是在barrier
的异步流程中,并不会每次都开启新的线程。
接下来在看一下任务的执行
1 | DISPATCH_NOINLINE |
先来看一组log
对于barrier
的异步实现比较巧妙,首先我们来想一个问题,要实现这种“异步等待”的策略,必然要使得barrier_async
那样具有wait的相关函数。
首先我们知道GCD
的队列都是FIFO 的,那么是否可以开启一个专用的栅栏线程,来执行这个FIFO的队列,从而形成一个类似barrier的功能呢。再来解释一下,当只有一个线程来调度队列时,因为队列FIFO的特性,使得这个队列的任务执行都是按顺序(前一个任务执行完,再执行后一个)。
在异步并发的场景下,GCD
的队列仍然遵循FIFO的规则,但是由于每一次dispatch_async
都会开辟一个线程,因此会有多个线程来执行多个队列任务。
A concurrent dispatch queue is useful when you have multiple tasks that can run in parallel. A concurrent queue is still a queue in that it dequeues tasks in a first-in, first-out order; however, a concurrent queue may dequeue additional tasks before any previous tasks finish.
值得注意的一点:barrier
对于全局队列无效。
barrier总结
在异步的场景,因为GCD
队列是FIFO
的,所以barrier_async
只要保证只有一个线程在 执行block 就形成了 栅栏。
在同步场景下,barrier
使用了和dispatch_sync
一样的wait
函数来实现同步。
但是barrier_sync
不能采用类似barrier_async
的做法:
- 同步函数没有开辟线程的能力。
- 同步是在当前线程执行栅栏,当前线程也有可能是多个异步(async 嵌套 sync 这样),因此这种情况下barrier_sync要有阻塞(wait)的能力。
semaphore
信号量是泛化的互斥体,互斥体只能是0和1,但信号量是:取值可以达到某个正数,即允许并发持有信号量的持有者的个数。
GCD
的信号量也是基于 mach 信号。
dispatch_semaphore_wait
1 | long |
dispatch_semaphore_wait
会使信号量原子性-1,然后进行等待或直接返回,等待的策略如下
1 | static long |
根据timeout
的参数不同,等待不同的时间,DISPATCH_TIME_FOREVER
会调用_dispatch_sema4_wait
。
1 | void |
与之配套的signal
1 | DISPATCH_NOINLINE |
信号量总体来说比较简单。值得注意的点就是当信号量销毁时,如果当前的信号值和初始值不一致,会引发crash
1 | void |
group
关于dispatch_group
的定义在code>semaphore.c
中,
1 | static inline dispatch_group_t |
group采用和信号量类似的思想,通过存储count的值来判断notify的操作。
而对应信号量的wait 和 signal有enter
leave
1 | void |
总体来说group
的实现方式比较简单,其中不乏借鉴了信号量的思想。
总结
API注意事项
多线程需要注意的点一个是多线程技术带来的数据竞争问题,一个是防止数据竞争带来的死锁问题。
死锁的条件
死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系。
对于GCD的同步API都应注意。
设计思想
架构分层
从GCD的整个流程扩展开来看,我们能够看到架构分层
设计思想:Mach
提供基础的原始API,由上层的BSD
实现不同的功能扩展。 也包括libdispatch
-> libpthread
-> xnu
的分层。
面向对象
在前面的malloc
源码也见过同样的设计,只不过由原来的zone
的继承,变成了GCD
的结构体嵌套。为了时这些队列操作对象都具有相同的行为,采用了vtable
的函数表设计,并关联在对应的结构体对象上,还有对应的类簇结构体。