这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
上一篇我们了解了GCD的队列和函数,这一篇我们主要探讨下日常的使用场景及其原理。GCD在我们日常多线程开发应该是最常用的形式,通常使用的有GCD单例,栅栏函数,信号量,定时器等,接下来一一分析。
1. GCD单例
1.1 单例的写法
单利通常有2种写法,第一种:
static Network *_network;
+(instancetype)share
{
if (!_network) {
_network = [[Network alloc]init];
}
return _network;
}
这样写会在多线程情况下导致重复创建,不安全。通常会添加一个同步锁
+(instancetype)share
{
@synchronized (self) {
if (!_network) {
_network = [[Network alloc]init];
}
}
return _network;
}
继续优化下,防止每次读取造成卡顿,外面在加一层判断。
+(instancetype)share
{
if (!_network) {
@synchronized (self) {
if (!_network) {
_network = [[Network alloc]init];
}
}
}
return _network;
}
第二种:看下GCD单例写法
static Network *_network;
+(instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_network = [[Network alloc]init];
});
return _network;
}
1.2 dispatch_once原理
那么dispatch_once的底层怎么实现的?
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
查看dispatch_once_f
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
#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);
}
先判断os_atomic_load的值是否为DLOCK_ONCE_DONE(执行过)则直接返回,否则进入_dispatch_once_gate_tryenter。任务执行过但是加锁失败DISPATCH_ONCE_IS_GEN(v),进入_dispatch_once_mark_done_if_quiesced把标识符v 设置DLOCK_ONCE_DONE。
继续查看没有执行过任务:_dispatch_once_gate_tryenter:
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
(uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}
判断对象是否储存过,os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识符置为DLOCK_ONCE_UNLOCKED。
static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
dispatch_function_t func)
{
_dispatch_client_callout(ctxt, func);
_dispatch_once_gate_broadcast(l);
}
没有储存过进行_dispatch_once_callout回调操作,执行过_dispatch_client_callout回调后,进行_dispatch_once_gate_broadcast广播。
static inline void
_dispatch_once_gate_broadcast(dispatch_once_gate_t l)
{
dispatch_lock value_self = _dispatch_lock_value_for_self();
uintptr_t v;
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
v = _dispatch_once_mark_quiescing(l);
#else
v = _dispatch_once_mark_done(l);//标记
#endif
if (likely((dispatch_lock)v == value_self)) return;
_dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);//通知
}
_dispatch_once_mark_done任务标识符标记为DLOCK_ONCE_DONE,如果此时有任务正在执行,再次进来一个任务,则通过_dispatch_once_wait函数让任务进入无限次等待。
1.3 单例总结
- 首先通过静态变量
onceTokend包装dispatch_once_gate_t类型l,通过os_atomic_load读取底层原子封装v,判断v的状态,即任务执行状态如果是DLOCK_ONCE_DONE表明任务已经执行过了,直接return不执行block。 - 任务的执行状态没有执行的话,进行加锁处理,即任务的标识符置为
DLOCK_ONCE_UNLOCKED。执行_dispatch_once_callout回调任务,执行完成结束,任务标识设置为DLOCK_ONCE_DONE,广播通知_dispatch_once_gate_broadcast。 - 有任务正在执行,再次进来一个任务,由于真正执行的任务加锁了,进入等待
_dispatch_once_wait。 验证下:oncetoken第一次执行后,任务标识设置为DLOCK_ONCE_DONE后面不在改变,单例化后返回的实例对象也是不变。
2. 栅栏函数
2.1 使用
GCD栅栏函数分为dispatch_barrier_async:前面的任务完成才会走到这里,dispatch_barrier_sync:作用相当,但是会堵塞,影响后面的任务执行。
- 异步栅栏:
作用于当前的并发队列,不影响后面队列的任务执行。当前并发队列在栅栏函数处的前面必须完成后才可以进行下面任务。 - 同步栅栏:
会堵塞线程,同步栅栏前面的完成了才可以进行下面的按正常的异步并发执行。 - 我们看下数组线程安全问题
在多线程中都对数组进行操作,我们知道赋值过程中会对
旧值release新值retain。之前setter方法底层实现有过探讨。这个时候因为异步操作,多个线程不断的对数组进行release和retain,如果同一时间2次release就自动销毁了,所以报这个错。
添加
异步栅栏函数,相当于添加了一个锁,同一时间只有一个线程对数组操作,起到了同步锁的作用
2.2 原理分析
2.2.1 异步栅栏函数
异步函数:
异步栅栏函数:
只有flag不同具体流程可以可以参考之前的
异步函数。
当前队列中还有其它任务进行
dx_wakeup操作,之后进行当前栅栏函数回调_dispatch_lane_barrier_complete。
- 我们把自定义队列换成全局队列,没有堵住按正常的异步函数执行。
看下官方描述
全局队
列系统也会使用,或者在别的地方也会使用。如果对全局队列生效的话,就会堵塞其它的全局队列中的线程。
2.2.2 同步栅栏函数
继续
_dispatch_barrier_sync_f->_dispatch_barrier_sync_f_inline
流程和同步函数类似。
获取线程id,检查死锁情况,执行回调。
查看:_dispatch_lane_barrier_sync_invoke_and_complete
3.信号量使用
dispatch_semaphore 型号量是线程同步操作的常用操作
dispatch_semaphore_t:信号量类型。
dispatch_semaphore_create:创建信号量。
dispatch_semaphore_wait:发送一个等待信号,信号量-1,当信号量为0的时候堵塞线程,大于0执行线程。
dispatch_semaphore_signal:发送唤醒信号,信号量+1。
我们日常开发中经常遇到一些请求是有顺序的,或者依赖关系的。但是异步请求是不确定请求顺序的,这时候我们使用dispatch_semaphore就可以解决。
3.1 dispatch_semaphore的使用
每次只会执行一个线程的回调,我们可以把
semaphore当钥匙,添加了dispatch_semaphore_wait就相当于一扇门。一开始我们默认0个钥匙可以执行的。任务2添加了一道门,任务3也是。任务1执行完把钥匙传下去,有肯能传到2也可能传到3,因为是异步函数。
想要实现同步也很简单
dispatch_semaphore_wait放到GCD外部就可以了,相当于同步函数。
3.2 dispatch_semaphore底层分析
dispatch_semaphore_t本质是dispatch_semaphore_s其中dsema_value代表当前信号量,dsema_orig表示初始信号量。
- 查看
dispatch_semaphore_create:主要初始化一个
DISPATCH_VTABLE的对象,并把型号量值塞进去, 指定了目标队列,这是一个优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT的非过载队列
#define _dispatch_get_default_queue(overcommit) \
_dispatch_root_queues[DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS + \
!!(overcommit)]._as_dq
- 继续看
dispatch_semaphore_wait
os_atomic_dec2o对信号量做-1操作,之后信号量大于等于0,就继续执行执行,不需要做操作;不满足的话执行_dispatch_semaphore_wait_slow
等待操作主要分为2种。1:
dispatch_time_t的类型是DISPATCH_TIME_NOW则获取当前信号量小于0的话,返回超时信号并把信号量+1;2:dispatch_time_t的类型是DISPATCH_TIME_FOREVER,则一直等待调用_dispatch_sema4_wait
- 查看
dispatch_semaphore_signal
信号量+1,信号量大于0什么也不做,继续执行。否则执行
_dispatch_semaphore_signal_slow
进行
唤醒线程操作,如果唤醒线程则返回非0,否则返回0继续查看_dispatch_sema4_signal
3.3 信号量总结
使用信号量可以让我们优雅的控制一些异步操作的前后顺序。比如我们请求大量的图片,请求完成后做操作。1:我们可以使用串行队列中执行
4. 调度组
dispatch_group常常用来同步多个任务(注意和dispatch_barrier_sync不同的是它可以是多个队列的同步),所以其实上面先分析dispatch_semaphore也是这个原因,它本身是依靠信号量来完成的同步管理。典型的用法如下
enter-leave:
dispatch_async:
4.1 使用分析
在enter-leave例子中,我们把enter和leave放入异步函数中就没有了同步的效果。
如上图所示3个异步函数执行从上到下,异步的导致执行顺序不是一定的。此时对于group的信号量是0
是平衡的,所以会直接执行。
值得注意的是注意enter和levae的搭配
enter多一次没关系,但是levae多一次的话会报错。
4.2 dispatch_group原理分析
dispatch_group_t本质是dispatch_group_s的结构体
typedef struct dispatch_group_s *dispatch_group_t;
dispatch_group_create
继续
创建
group对象,n指定为0,给group赋值,指定默认队列。
dispatch_group_enter
主要是
os_atomic_sub_orig2o对dg_bits进行-1操作
dispatch_group_leave
主要是
os_atomic_sub_orig2o对dg_bits进行+1操作;根据状态,do-while循环,唤醒执行block任务;多次调用leave会导致崩溃。
查看_dispatch_group_wake
如果有
notify等待则执行notify遍历并且在对应队列中执行,如果有wait任务则唤醒其执行任务
dispatch_group_async
和异步函数类似,继续看
_dispatch_continuation_group_async
进组操作,
_dispatch_continuation_async前面已经介绍过。既然有enter,必然要leave。猜测执行完notify的block后进行leave操作。
搜素
_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke中
dispatch_group_notify如果old_state等于0,就可以进行释放了除了
leave可以进行唤醒操作,notify也可以唤醒操作。简单的说就是dispatch_group_async和dispatch_group_notify本身就是和dispatch_group_enter、dispatch_group_leave没有本质区别,后者相对更加灵活。dispatch_group_wait:
os_atomic_rmw_loop2o不断遍历,如果(old_state & DISPATCH_GROUP_VALUE_MASK) == 0表示执行完,直接返回0;如果当前如果超时立即返回;其他情况调用_dispatch_group_wait_slow
实际使用
5. 定时器&延时操作
5.1 使用
5.1.1 主线程添加定时器
子线程添加定时器要手动开启
[[NSRunLoop currentRunLoop]run],主线程runloop是自动开启的。
2. GCD使用定时器
使用过程注意的点:
- 定时期手动开启,
dispatch_resume,暂停:dispatch_suspend。销毁:dispatch_source_cancel,开启和暂停,开启和销毁最好时配对出现,比如连续调用2次dispatch_resume,代码中的if分支不成立,执行DISPATCH_CLIENT_CRASH造成崩溃。
- 定时器要被当前对象持有,否则会被释放
depose导致无法运行,最好使用属性修饰长久持有。
@property (nonatomic, weak) dispatch_source_t timer1
-
定时器最好时采用使用
懒加载创建定时器,并且记录当timer处于dispatch_suspend的状态。这些时候,只要在 调用dealloc时判断下,已经调用过dispatch_suspend则再调用下dispatch_resume后再cancel,然后再释放timer -
block中注意相互持有,循环引用导致内存泄露,无法释放。
5.1.2. 延时调用
主要2种
perform 和 dispatch_after
5.2 原理分析
5.2.1 GCD定时器
dispatch_source_t
dispatch_source_create(dispatch_source_type_t dst, uintptr_t handle,
uintptr_t mask, dispatch_queue_t dq)
{
dispatch_source_refs_t dr;//事件源状态其中可能包含对源对象的引用
dispatch_source_t ds;//事件源
dr = dux_create(dst, handle, mask)._dr;//创建事件源状态
if (unlikely(!dr)) {
return DISPATCH_BAD_INPUT;
}
ds = _dispatch_queue_alloc(source,
dux_type(dr)->dst_strict ? DSF_STRICT : DQF_MUTABLE, 1,
DISPATCH_QUEUE_INACTIVE | DISPATCH_QUEUE_ROLE_INNER)._ds;
ds->dq_label = "source";
ds->ds_refs = dr;
dr->du_owner_wref = _dispatch_ptr2wref(ds);//初始化事件源,并赋值
if (unlikely(!dq)) {
dq = _dispatch_get_default_queue(true);//不存在获取默认队列
} else {
_dispatch_retain((dispatch_queue_t _Nonnull)dq);
}
ds->do_targetq = dq;//事件源关联队列
if (dr->du_is_timer && (dr->du_timer_flags & DISPATCH_TIMER_INTERVAL)) {
dispatch_source_set_timer(ds, DISPATCH_TIME_NOW, handle, UINT64_MAX);//定时器类型间隔时间无限大
}
_dispatch_object_debug(ds, "%s", __func__);
return ds;
}
-
创建调度源(调度源主要是监听系统特定的一些事件发生,调度源捕捉到后进行相关处理,我们在回调函数中做逻辑处理),主要有4个参数:
type:第一个类型我们监听事件的类型
`DISPATCH_SOURCE_TYPE_DATA_ADD`:属于自定义事件,可以通过`dispatch_source_get_data`函数获取事件变量数据,在我们自定义的方法中可以调用`dispatch_source_merge_data`函数向Dispatch Source设置数据,下文中会有详细的演示。 `DISPATCH_SOURCE_TYPE_DATA_OR`:属于自定义事件,用法同上面的类型一样。 `DISPATCH_SOURCE_TYPE_MACH_SEND`:Mach端口发送事件。 `DISPATCH_SOURCE_TYPE_MACH_RECV`:Mach端口接收事件。 `DISPATCH_SOURCE_TYPE_PROC`:与进程相关的事件。 `DISPATCH_SOURCE_TYPE_READ`:读文件事件。 `DISPATCH_SOURCE_TYPE_WRITE`:写文件事件。 `DISPATCH_SOURCE_TYPE_VNODE`:文件属性更改事件。 `DISPATCH_SOURCE_TYPE_SIGNAL`:接收信号事件。 `DISPATCH_SOURCE_TYPE_TIMER`:定时器事件。 `DISPATCH_SOURCE_TYPE_MEMORYPRESSURE`:内存压力事件。handle:第二个参数是取决于要监听的事件类型,比如如果是监听Mach端口相关的事件,那么该参数就是mach_port_t类型的Mach端口号,如果是监听事件变量数据类型的事件那么该参数就不需要,设置为0就可以了。mask:第三个参数同样取决于要监听的事件类型,比如如果是监听文件属性更改的事件,那么该参数就标识文件的哪个属性,比如DISPATCH_VNODE_RENAME。queue:第四个参数设置回调函数所在的队列。
-
查看
dispatch_source_set_timer
设置定时器源的
起始时间、间隔时间和偏差值。如果计时器源已经被取消,调用这个函数没有效果。
- 查看
dispatch_source_set_event_handler
查看_dispatch_source_handler_alloc回调函数
设置闭包形式的处理器,该
回调函数或闭包就是Dispatch Source的事件处理器.
5.2.2 dispatch_after分析
void
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t work)
{
_dispatch_after(when, queue, NULL, work, true);
}
// 查看 _dispatch_after
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t dq,
void *ctxt, void *handler, bool block)
{
dispatch_timer_source_refs_t dt;
dispatch_source_t ds;
uint64_t leeway, delta;
if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
DISPATCH_CLIENT_CRASH(0, "dispatch_after called with 'when' == infinity");
#endif
return;
}
delta = _dispatch_timeout(when);
if (delta == 0) {
if (block) {
return dispatch_async(dq, handler);
}
return dispatch_async_f(dq, ctxt, handler);
}
leeway = delta / 10; // <rdar://problem/13447496>
if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;
// this function can and should be optimized to not use a dispatch source
ds = dispatch_source_create(&_dispatch_source_type_after, 0, 0, dq);
dt = ds->ds_timer_refs;
dispatch_continuation_t dc = _dispatch_continuation_alloc();
if (block) {
_dispatch_continuation_init(dc, dq, handler, 0, 0);
} else {
_dispatch_continuation_init_f(dc, dq, ctxt, handler, 0, 0);
}
// reference `ds` so that it doesn't show up as a leak
dc->dc_data = ds;
_dispatch_trace_item_push(dq, dc);
os_atomic_store2o(dt, ds_handler[DS_EVENT_HANDLER], dc, relaxed);
dispatch_clock_t clock;
uint64_t target;
_dispatch_time_to_clock_and_value(when, &clock, &target);
if (clock != DISPATCH_CLOCK_WALL) {
leeway = _dispatch_time_nano2mach(leeway);
}
dt->du_timer_flags |= _dispatch_timer_flags_from_clock(clock);
dt->dt_timer.target = target;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_timer.deadline = target + leeway;
dispatch_activate(ds);
}
无时间差则直接调用dispatch_async,否则先创建一个dispatch_source_t,不同的是这里的类型并不是DISPATCH_SOURCE_TYPE_TIMER而是_dispatch_source_type_after,查看源码不难发现它只是dispatch_source_type_s类型的一个常量和_dispatch_source_type_timer并没有明显区别:
而和
dispatch_activate()其实和dispatch_resume() 是一样的开启定时器.在上面的定时器在_dispatch_source_set_handler封装成一个dispatch_continuation_t进行同步或者异步调用,而上面_dispatch_after直接构建了dispatch_continuation_t进行执行。
6. 总结
以上就是GCD实际使用及其大概原理,GCD在oc中大量广泛的应用,主要包括单例(确保了多线程的安全),栅栏函数(异步函数中确保有的操作的前后顺序),信号量(通过信号量的加减达到异步函数的同步效果),调度组(原理和信号量类似,比如多个请求完成后刷新主界面,通过enter和leave进行notify回调),调度源(11种类型,使用最多的之一是定时器,相比NSTimer提高准确性。)最后分享使用objc.io上的一幅图表示如下:
GCD队列和线程原理和应用大致就探索完了,有不对的地方还望见谅。