本篇章从源码的角度来探索单例,栅栏函数、信号量、调度组、事件源
一:单例 dispatch_once
单例我们在开发中也是使用非常频繁,其中我们使用到了GCD的dispatch_once函数
static dispatch_once_t token;
dispatch_once(&token, ^{
// code
});
定义如下:
#define dispatch_once _dispatch_once
在libdispatch中找到_dispatch_once
这里对不同情况进行处理,dispatch_compiler_barrier()是对栅栏的处理,直接找到dispatch_once
最后调用dispatch_once_f
- 将传入
val包装成l,通过os_atomic_load从底层取出,关联到变量v上。如果v这个值等于DLOCK_ONCE_DONE,也就是已经处理过一次了,就会直接return返回。
_dispatch_once_gate_tryenter
在_dispatch_once_gate_tryenter中进行原子操作,就是锁的处理,所有它是线程安全的。如果之前没有执行过,原子处理会比较它状态,进行解锁,最终会返回一个bool值,多线程情况下,只有一个能够获取锁返回yes。
如果最终返回yes,则会调用_dispatch_once_callout,执行单例任务并对外广播。
看下_dispatch_once_gate_broadcast
将token通过原子比对,如果不是done,则设为done。同时对_dispatch_once_gate_tryenter方法中的锁进行处理。
_dispatch_once_mark_done
当标记为done之后,下次进来就直接返回了。
看上图中其中最还有一个_dispatch_once_wait,这是对多线程的处理,如果存在多线程,且没有获取到锁,就会调用_dispatch_once_wait,进行等待,这里开启了自旋锁,内部进行原子处理,在loop过程中,如果发现已经被其他线程设置once_done了,则会进行放弃处理。
一张图来概括:
二:栅栏 dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async 方法在两个操作组间形成栅栏。
2.1 基本使用
dispatch_barrier_async
- (void)demo2{
dispatch_queue_t concurrentQueue = dispatch_queue_create("cc", DISPATCH_QUEUE_CONCURRENT);
/* 1.异步函数 */
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"123");
});
dispatch_async(concurrentQueue, ^{
sleep(2);
NSLog(@"456");
});
/* 2. 栅栏函数 */ // - dispatch_barrier_sync
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
NSLog(@"789");
});
/* 3. 异步函数 */
dispatch_async(concurrentQueue, ^{
NSLog(@"加载那么多,喘口气!!!");
});
NSLog(@"**********起来干!!");
}
打印结果如下:
我们发现
dispatch_barrier_async并不会阻塞主线程,所以结尾最先打印。但是会阻塞任务线程concurrentQueue,所以123和456优先于789打印,最后打印加载那么多,喘口气。
dispatch_barrier_sync
还是上述示例代码,将dispatch_barrier_async换成dispatch_barrier_sync之后,我来看下打印结果。
唯一的不同在于结尾变成了最后打印,也就是说dispatch_barrier_sync会阻塞当前线程也就是主线程,这点在使用过程中需要注意。
-
注意事项
- 栅栏函数和其他的任务必须在同一个队列中。
- 只能使用自定义的并发队列,不能使用全局并发队列。
2.2 原理分析
dispatch_barrier_sync
void
dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}
跟踪源码来到_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline
其中_dispatch_queue_try_acquire_barrier_sync判断挂起,最终来到_dispatch_queue_try_acquire_barrier_sync_and_suspend,会对当前的状态state加一层处理,暂时放弃。
最终返回的是下层的OS控制处理。
另外,这里_dispatch_sync_f_slow还涉及到了死锁的处理,。
示例代码主线程同步造成死锁,查看调用堆栈涉及 _dispatch_sync_f_slow和__DISPATCH_WAIT_FOR_QUEUE__
_dispatch_sync_f_slow
在该方法中,会将任务添加到队列,以主线程添加同步任务为例,会将同步任务添加到主队列。
__DISPATCH_WAIT_FOR_QUEUE__
可以理解为A任务要执行,可是之前又被系统安排为等待,那到底是执行还是等待呢?就产生了矛盾,相互等待,造成死锁。
我们回到_dispatch_barrier_sync_f_inline往下看。
根据堆栈信息,发现会调用_dispatch_sync_invoke_and_complete_recurse
训着这条轨迹,跟随这条调用路线 _dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse
这里是一个 do while循环,判断当前队列里面是否有barrier,有的话就dx_wakeup唤醒执行,直到任务执行完成了,才会执行_dispatch_lane_non_barrier_complete,表示当前队列任务已经执行完成了,并且没有栅栏函数了就会继续往下面的流程走。
#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
这里我们直接搜索dq_wakeup
根据不同的队列,走不同的方法,全局并发的是_dispatch_root_queue_wakeup,串行和并发的是_dispatch_lane_wakeup。
首先看下自定义并发队列的_dispatch_lane_wakeup
- 判断是否为
barrier形式的,会调用_dispatch_lane_barrier_complete方法处理 - 如果没有
barrier形式的,则走正常的并发队列流程,调用_dispatch_queue_wakeup方法。
_dispatch_lane_barrier_complete
-
如果是串行队列,则会进行等待,等待其他的任务执行完成,再按顺序执行
-
如果是并发队列,则会调用
_dispatch_lane_drain_non_barriers方法将栅栏之前的任务执行完成。 -
最后会调用
_dispatch_lane_class_barrier_complete方法,也就是把栅栏拔掉了,不拦了,从而执行栅栏之后的任务。
再来看全局并发队列的 _dispatch_root_queue_wakeup
-
全局并发队列这个里面,并没有对
barrier的判断和处理,就是按照正常的并发队列来处理。 -
全局并发队列为什么没有对栅栏函数进行处理呢?因为全局并发队列除了被我们使用,系统也在使用。
-
如果添加了栅栏函数,会导致队列运行的阻塞,从而影响系统级的运行,所以栅栏函数也就不适用于全局并发队列
_dispatch_barrier_sync_f_inline的最后,会来到_dispatch_lane_barrier_sync_invoke_and_complete对已经执行完成的任务进行下次层状态的释放。
三:信号量 dispatch_semaphore
GCD的使用过程中是无法控制并发数量的,但是我们可以"曲线救国",使用信号量来解决这个问题。
信号量Dispatch Semaphore,是一种持有计数的信号的东西。有如下三个方法。
dispatch_semaphore_create: 创建一个 Semaphore 并初始化信号的总量dispatch_semaphore_signal: 信号量发送,让信号总量加 1dispatch_semaphore_wait: 信号量等待,可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
3.1 基本使用
- (void)test {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务1");
sleep(1);
NSLog(@"任务1完成");
dispatch_semaphore_signal(sem);
});
//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务2");
sleep(1);
NSLog(@"任务2完成");
dispatch_semaphore_signal(sem);
});
//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务3");
sleep(1);
NSLog(@"任务3完成");
dispatch_semaphore_signal(sem);
});
}
打印结果:
- 我们将初始的信号量设置为
1,也就是控制了最大并发数就是1
3.2 原理分析
3.2.1 dispatch_semaphore_create
dispatch_semaphore_create主要是初始化信号量,并设置GCD的最大并发数,其最大并发数必须大于等于0
3.2.2 dispatch_semaphore_signal
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema){
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
os_atomic_inc2o是原子操作自增加1,然后会判断,如果value > 0,就会返回0。但是如果加一次后依然小于0,则会报异常:Unbalanced call to dispatch_semaphore_signal(),然后会调用_dispatch_semaphore_signal_slow方法的,做下层的处理,进入长等待。
3.2.3 dispatch_semaphore_wait
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout){
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
os_atomic_dec2o是原子操作自减1,然后会判断value >= 0,返回成功。如果结果小于0,则会调用_dispatch_semaphore_wait_slow进行长等待。
_dispatch_semaphore_wait_slow 会根据传入的超时时间timeout,进行分开处理。
整体图:
四:调度组 dispatch_group
4.1 基本使用
用法一
- (void)demoTest {
// 调度组
dispatch_group_t group = dispatch_group_create();
// 队列
dispatch_queue_t queue = dispatch_queue_create("cc", DISPATCH_QUEUE_CONCURRENT);
// 将任务添加到队列和调度组
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"下载 A %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"下载 B %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"下载 C %@",[NSThread currentThread]);
});
// 异步 : 调度组中的所有异步任务执行结束之后,在这里得到统一的通知
dispatch_group_notify(group, queue, ^{
NSLog(@"下载完成 %@",[NSThread currentThread]);
});
// 同步 : 一直等到调度组中所有的任务都执行结束以后才执行后面的代码
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 验证调度组是否是异步
NSLog(@"end");
}
- 当A,B,C 任务完成之后才会打印
下载完成
用法二
使用dispatch_group_enter和dispatch_group_leave
/**
* 队列组 dispatch_group_enter、dispatch_group_leave
*/
- (void)groupEnterAndLeave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程.
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
}
dispatch_group_enter、dispatch_group_leave 组合,其实等同于dispatch_group_async,但是两者必须成对出现。
4.2 源码分析
4.2.1 dispatch_group_create
dispatch_group_create
dispatch_group_t
dispatch_group_create(void){
return _dispatch_group_create_with_count(0);
}
会调用_dispatch_group_create_with_count,并传入固定默认值0,内部进行组的定义和赋值操作。
static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n){
// 🌹 定义
dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
sizeof(struct dispatch_group_s));
// 🌹 赋值操作
dg->do_next = DISPATCH_OBJECT_LISTLESS;
dg->do_targetq = _dispatch_get_default_queue(false);
if (n) {
os_atomic_store2o(dg, dg_bits,
(uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
}
return dg;
}
4.2.2 dispatch_group_enter
void
dispatch_group_enter(dispatch_group_t dg)
{
// The value is decremented on a 32bits wide atomic so that the carry
// 🌹 for the 0 -> -1 transition is not propagated to the upper 32bits.
uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
DISPATCH_GROUP_VALUE_INTERVAL, acquire);
uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
if (unlikely(old_value == 0)) {
_dispatch_retain(dg); // <rdar://problem/22318411>
}
if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
DISPATCH_CLIENT_CRASH(old_bits,
"Too many nested calls to dispatch_group_enter()");
}
}
os_atomic_sub_orig2o会对dg->dg.bits进行减的操作,0->-1的变化。
4.2.3 dispatch_group_leave
void
dispatch_group_leave(dispatch_group_t dg)
{
// The value is incremented on a 64bits wide atomic so that the carry for
// 🌹 the -1 -> 0 transition increments the generation atomically.
uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,//原子递增 ++
DISPATCH_GROUP_VALUE_INTERVAL, release);
uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);
//🌹 根据状态,唤醒
if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
old_state += DISPATCH_GROUP_VALUE_INTERVAL;
do {
new_state = old_state;
if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
} else {
// If the group was entered again since the atomic_add above,
// we can't clear the waiters bit anymore as we don't know for
// which generation the waiters are for
new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
}
if (old_state == new_state) break;
} while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
old_state, new_state, &old_state, relaxed)));
return _dispatch_group_wake(dg, old_state, true);//唤醒
}
//-1 -> 0, 0+1 -> 1,即多次leave,会报crash,简单来说就是enter-leave不平衡
if (unlikely(old_value == 0)) {
DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
"Unbalanced call to dispatch_group_leave()");
}
}
os_atomic_add_orig2o会进行自增的操作,old_state就为0,oldvalue也是0,根据查看DISPATCH_GROUP_VALUE_1,old_value == DISPATCH_GROUP_VALUE_1)条件是不成立的,最终会来到_dispatch_group_wake进行唤醒,唤醒的就是dispatch_group_notify方法。
换句话说,如果不调用dispatch_group_leave方法,也就不会唤醒dispatch_group_notify,下面的流程也就不会执行了。大家可以试试只进组不出组的情况,dispatch_group_notify肯定不会被调用。
4.2.4 dispatch_group_notify
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dsn)
{
uint64_t old_state, new_state;
dispatch_continuation_t prev;
dsn->dc_data = dq;
_dispatch_retain(dq);
//🌹 获取dg底层的状态标识码,通过os_atomic_store2o获取的值,即从dg的状态码 转成了 os底层的state
prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
if (os_mpsc_push_was_empty(prev)) {
os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
if ((uint32_t)old_state == 0) { //如果等于0,则可以进行释放了
os_atomic_rmw_loop_give_up({
return _dispatch_group_wake(dg, new_state, false);//唤醒
});
}
});
}
}
在old_state等于0的情况下,才会去唤醒相关的同步或者异步函数的执行,也就是 block里面的执行。
-
在上面
dispatch_group_leave分析中,我们已经得到old_state结果等于0。 -
所以这里也就解释了
dispatch_group_enter和dispatch_group_leave为什么要配合起来使用的原因,通过这种控制,有加有减,避免异步的影响,能够及时唤醒并调用dispatch_group_notify方法 -
我们注意到在
dispatch_group_leave里面也有调用_dispatch_group_wake方法,这是因为异步的执行,任务是执行耗时的,有可能dispatch_group_leave这行代码还没有走,就先走了dispatch_group_notify方法,但这时候dispatch_group_notify方法里面的任务并不会执行,只是把任务添加到group。 -
它会等
dispatch_group_leave执行了被唤醒才执行,这样就保证了异步时,dispatch_group_notify里面的任务不丢弃,可以正常执行。
4.2.5 dispatch_group_async
上面提到过dispatch_group_enter、dispatch_group_leave 组合,其实等同于dispatch_group_async,下面我们跟着源码看看是不是这样?
void
dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_block_t db)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
dispatch_qos_t qos;
//🌹任务包装器
qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
//🌹处理任务
_dispatch_continuation_group_async(dg, dq, dc, qos);
}
_dispatch_continuation_group_async封装了进组的操作
static inline void
_dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dc, dispatch_qos_t qos)
{
dispatch_group_enter(dg);//🌹进组
dc->dc_data = dg;
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);//🌹异步操作
}
按照猜想有进就有出。
- (void)groupDemo{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
//创建调度组
🌹 任务1
NSLog(@"任务1");
sleep(1);
dispatch_group_leave(group);
});
dispatch_group_async(group, queue, ^{;
🌹 任务2
NSLog(@"任务2");
});
dispatch_group_notify(group, dispatch_get_main_queue(),^{
NSLog(@"终于到我执行了");
});
}
在任务2打上断点,查看堆栈,调用了_dispatch_client_callout
全局搜索_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke进行了调用。
总结
enter-leave只要成对就可以,不管远近dispatch_group_enter在底层是通过C++函数,对group的value进行--操作(即0 -> -1)dispatch_group_leave在底层是通过C++函数,对group的value进行++操作(即-1 -> 0)dispatch_group_notify在底层主要是判断group的state是否等于0,当等于0时,就通知- block任务的唤醒,可以通过
dispatch_group_leave,也可以通过dispatch_group_notify dispatch_group_async等同于enter - leave,其底层的实现就是enter-leave
五:事件源 dispatch_source
Dispatch Source 是BSD系统内核惯有功能kqueue的包装,kqueue是在XNU内核中发生事件时在应用程序编程方执行处理的技术。
它的CPU负荷非常小,尽量不占用资源。当事件发生时,Dispatch Source 会在指定的Dispatch Queue中执行事件的处理。
dispatch_source_create:创建源dispatch_source_set_event_handler: 设置源的回调dispatch_source_merge_data: 源事件设置数据dispatch_source_get_data: 获取源事件的数据dispatch_resume:恢复继续dispatch_suspend:挂起
我们在日常开发中,经常会使用计时器NSTimer,例如发送短信的倒计时,或者进度条的更新。但是NSTimer需要加入到NSRunloop中,还受到mode的影响。收到其他事件源的影响,被打断,当滑动scrollView的时候,模式切换,定时器就会停止,从而导致timer的计时不准确。
GCD提供了一个解决方案dispatch_source来出来类似的这种需求场景。
- 时间较准确,
CPU负荷小,占用资源少 - 可以使用子线程,解决定时器跑在主线程上卡UI问题
- 可以暂停,继续,不用像
NSTimer一样需要重新创建
使用示例 - 定时器
- (void)use033{
//倒计时时间
__block int timeout = 3;
//创建队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
//创建timer
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
//设置1s触发一次,0s的误差
/*
- source 分派源
- start 数控制计时器第一次触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。
- interval 间隔时间
- leeway 计时器触发的精准程度
*/
dispatch_source_set_timer(timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0);
//触发的事件
dispatch_source_set_event_handler(timer, ^{
//倒计时结束,关闭
if (timeout <= 0) {
//取消dispatch源
dispatch_source_cancel(timer);
}else{
timeout--;
dispatch_async(dispatch_get_main_queue(), ^{
//更新主界面的操作
NSLog(@"倒计时 - %d", timeout);
});
}
});
//开始执行dispatch源
dispatch_resume(timer);
}
-
使用定时器
NSTimer需要加入到NSRunloop,导致计数不准确,可以使用Dispatch Source来解决 -
Dispatch Source的使用,要注意恢复和挂起要平衡 -
source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建。 -
因为
dispatch_source_set_event_handle回调是block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。所以正确的方法是使用weak+strong或者提前调dispatch_source_cancel取消timer。
参考:
iOS底层探索之多线程(十二)—GCD源码分析(事件源dispatch_source)
iOS GCD 之 底层原理分析