持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
今天我们继续探索GCD,单例的原理,什么是栅栏函数,什么是信号量,dispatch_source的运用等。
一. GCD线程队列探索下
1、单例的原理
现在就来看看平时我们写的单例代码:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
//进入源码
#ifdef __BLOCKS__
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
#endif
/////////////////////////////////////////////////////////////////////////
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
//dispatch_once_t强转dispatch_once_gate_t
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
//判断func 是否被执行,被执行直接return
#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
return _dispatch_once_wait(l);
}
只有
&l->dgo_once的状态等于DLOCK_ONCE_UNLOCKED的时候才会进行_dispatch_once_callout的操作。
_dispatch_once_callout->_dispatch_once_gate_broadcast->_dispatch_once_mark_done
static inline uintptr_t
_dispatch_once_mark_done(dispatch_once_gate_t dgo)
{
//&dgo->dgo_once把状态赋值成DLOCK_ONCE_DONE
return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
}
把&dgo->dgo_once = DLOCK_ONCE_DONE;//标记
2、栅栏函数
dispatch_barrier_async栅栏函数的效果:等待栅栏函数前添加到队列⾥⾯的任务全部执⾏完成之后,才会执⾏栅栏函数⾥⾯的任务,栅栏函数⾥⾯的任务执⾏完成之后才会执⾏栅栏函数后⾯的队列⾥⾯的任务。
需要注意的点:
-
栅栏函数只对
同⼀队列起作⽤。 -
栅栏函数对
全局并发队列⽆效。
通过例子来充分理解:
dispatch_queue_t t = dispatch_queue_create("ny", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
NSLog(@"2");
});
// 栅栏函数
dispatch_barrier_async(t, ^{
NSLog(@"3");
});
NSLog(@"4");
dispatch_async(t, ^{
NSLog(@"5");
});
创建了一个并发队列t,添加了异步打印1,2,添加栅栏函数异步打印3,添加异步打印5,会先打印4,1,2打印412顺序位置不确定,完打印3,然后打印5 打印 41235,12435,14235等
打印结果和我们分析的一样。
继续看例子二:
dispatch_queue_t t = dispatch_queue_create("ny", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t t2 = dispatch_get_global_queue(0, 0);
dispatch_async(t2, ^{
NSLog(@"1");
});
dispatch_async(t2, ^{
sleep(3);
NSLog(@"2");
});
// 栅栏函数
dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
sleep(4);
NSLog(@"3");
});
NSLog(@"4");
dispatch_async(t2, ^{
NSLog(@"5");
});
创建t2全局并发队列,然后添加异步打印1,2 (sleep 3秒),然后添加栅栏函数打印3 (sleep 4秒),打印4,打印5. 打印结果:
发现
栅栏函数并没有拦住12的打印,让5打印在3之后。验证了,我们之前的结论,栅栏函数无法拦住全局并发队列。为什么拦截不了呢?继续深入探索。
进入源码libdispatch-1271:搜索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);
}
void
dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func)
{
_dispatch_barrier_sync_f_inline(dq, ctxt, func, 0);
}
我们通过符号断点,来跟踪一下这个几个函数的运行。
发现运行到了
_dispatch_sync_f_slow的符号断点中.
通过_dispatch_sync_f_slow->_dispatch_sync_function_invoke->_dispatch_sync_function_invoke_inline->_dispatch_client_callout:
我们继续符号断点,进行反正法推导验证:
dispatch_queue_t t = dispatch_queue_create("ny", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t t2 = dispatch_get_global_queue(0, 0);
dispatch_async(t, ^{
NSLog(@"1");
});
dispatch_async(t, ^{
sleep(30);
NSLog(@"2");
});
// 栅栏函数
dispatch_barrier_sync(t, ^{
sleep(5);
NSLog(@"3");
});
NSLog(@"4");
dispatch_async(t, ^{
NSLog(@"5");
});
继续执行。
在同步栅栏函数执行时,会
等待异步队列t中的 1,2打印执行完成才执行。同时栅栏函数执行完才能执行队列后面的函数。
然后我们使用,全局并发队列再次运行。
在全局并发队列中,
栅栏函数执行并没等待队列任务2执行完成。就马上执行了打印3.
3、group调度组
调度组的效果:等待调度组前⾯的任务执⾏完才会执⾏dispatch_group_notify函数⾥⾯的任务。
调度组和队列没有关系,只要是同⼀调度组就可以。
分析例子:
dispatch_group_t g = dispatch_group_create();
dispatch_queue_t que1 = dispatch_queue_create("l1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t que2 = dispatch_queue_create("l2", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(g);
dispatch_async(que1, ^{
sleep(2);
NSLog(@"1");
dispatch_group_leave(g);
});
dispatch_group_enter(g);
dispatch_async(que2, ^{
sleep(3);
NSLog(@"2");
dispatch_group_leave(g);
});
dispatch_group_enter(g);
dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
sleep(4);
NSLog(@"3");
dispatch_group_leave(g);
});
dispatch_group_enter(g);
dispatch_async(dispatch_get_main_queue(), ^{
sleep(5);
NSLog(@"4");
dispatch_group_leave(g);
});
dispatch_group_notify(g, dispatch_get_global_queue(0, 0), ^{
NSLog(@"5");
});
打印就是 1234 执行完了才会执行打印5. 然后1234是有序的。
dispatch_group_enter 和 dispatch_group_leave 类似信号量的+,- 。然后等于0时执行dispatch_group_notify
4、什么是信号量
信号量dispatch_semaphore
dispatch_semaphore主要就是三个⽅法:
-
dispatch_semaphore_create(long value);这个函数是创建⼀个dispatch_semaphore_t类型的信号量,并且创建的时候需要指定信号量的⼤⼩。 -
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);等待信号量。如果信号量值为0,那么该函数就会⼀直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值⼤于等于1,该函数会对信号量的值进⾏减1操作,然后返回。
3.dispatch_semaphore_signal(dispatch_semaphore_t deem);发送信号量。该函数会对信号量
的值进⾏加1操作。
通过这三个⽅法,就能控制GCD的最⼤并发数量。信号量在使⽤的时候需要注意: dispatch_semaphore_wait 和 dispatch_semaphore_signal ⼀定
要成对出现。因为在信号量释放的时候,如果dsema_orig初始信号量的⼤⼩⼤于dsema_value(通
过dispatch_semaphore_wait和dispatch_semaphore_signal改变之后的信号量的⼤⼩)就会触发崩
溃。
通过一个例子进行分析:
dispatch_semaphore_t sem = dispatch_semaphore_create(0); //创建初始信号量
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //加入全局并发队列
NSLog(@"1");
dispatch_semaphore_signal(sem); //信号+1 1
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);//如果信号量值为0,那么该函数就会⼀直等待
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
dispatch_semaphore_signal(sem);//信号+1
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"3");
dispatch_semaphore_signal(sem);//信号+1
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"4");
dispatch_semaphore_signal(sem);//信号+1
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"5");
dispatch_semaphore_signal(sem);//信号+1
});
分析:sem=0 执行添加全局队列任务并 执行打印1,然后等待->任务1执行完成并对sem+1,然后dispatch_semaphore_wait不在等待进行任务2 并且对sem-1,在任务2 执行完时dispatch_semaphore_signal对sem+1 ,如此反复运行到任务5.所以打印有顺序 12345.
我们进入源码一探究竟:
继续探索
dispatch_semaphore_wait
_dispatch_semaphore_wait_slow->_dispatch_sema4_wait
5、dispatch_source
dispatch_source是⽤来监听事件的,可以创建不同类型的dispatch_source来监听不同的事件。
dispatch_source可以监听的事件类型:
dispatch_source的具体⽤法:在任⼀线程上调⽤它的dispatch_source_merge_data函数,会执⾏
dispatch_source事先定义好的句柄(可以把句柄简单理解为⼀个block)。
dispatch_source的⼏个⽅法:
定时器监听小例子:
总结:
栅栏函数的效果:等待栅栏函数前添加到队列⾥⾯的任务全部执⾏完成之后,才会执⾏栅栏函数⾥
⾯的任务,栅栏函数⾥⾯的任务执⾏完成之后才会执⾏栅栏函数后⾯的队列⾥⾯的任务。
需要注意的点:
-
栅栏函数只对同⼀队列起作⽤。
-
栅栏函数对全局并发队列⽆效。
调度组的效果:等待调度组前⾯的任务执⾏完才会执⾏dispatch_group_notify函数⾥⾯的任务。
调度组和队列没有关系,只要是同⼀调度组就可以。
dispatch_semaphore主要就是三个⽅法:
-
dispatch_semaphore_create(long value);这个函数是创建⼀个dispatch_semaphore_t类型的信号量,并且创建的时候需要指定信号量的⼤⼩。 -
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);等待信号量。如果信号量值为0,那么该函数就会⼀直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值⼤于等于1,该函数会对信号量的值进⾏减1操作,然后返回。
3.dispatch_semaphore_signal(dispatch_semaphore_t deem);发送信号量。该函数会对信号量
的值进⾏加1操作。
通过这三个⽅法,就能控制GCD的最⼤并发数量。
信号量在使⽤的时候需要注意: dispatch_semaphore_wait 和 dispatch_semaphore_signal ⼀定要成对出现。因为在信号量释放的时候,如果dsema_orig初始信号量的⼤⼩⼤于dsema_value(通
过dispatch_semaphore_wait和dispatch_semaphore_signal改变之后的信号量的⼤⼩)就会触发崩
溃。
dispatch_source是⽤来监听事件的,可以创建不同类型的dispatch_source来监听不同的事件。