底层原理-23-GCD分析(下)

1,137 阅读11分钟

这是我参与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 单例总结

  1. 首先通过静态变量onceTokend包装dispatch_once_gate_t类型l,通过os_atomic_load读取底层原子封装v,判断v的状态,即任务执行状态如果是DLOCK_ONCE_DONE表明任务已经执行过了,直接return不执行block
  2. 任务的执行状态没有执行的话,进行加锁处理,即任务的标识符置为DLOCK_ONCE_UNLOCKED。执行_dispatch_once_callout回调任务,执行完成结束,任务标识设置为DLOCK_ONCE_DONE,广播通知_dispatch_once_gate_broadcast
  3. 有任务正在执行,再次进来一个任务,由于真正执行的任务加锁了,进入等待_dispatch_once_wait。 验证下: image.png oncetoken第一次执行后,任务标识设置为DLOCK_ONCE_DONE后面不在改变,单例化后返回的实例对象也是不变。

2. 栅栏函数

2.1 使用

GCD栅栏函数分为dispatch_barrier_async:前面的任务完成才会走到这里,dispatch_barrier_sync:作用相当,但是会堵塞,影响后面的任务执行。

  • 异步栅栏:作用于当前的并发队列,不影响后面队列的任务执行当前并发队列在栅栏函数处的前面必须完成后才可以进行下面任务。 image.png
  • 同步栅栏:会堵塞线程,同步栅栏前面的完成了才可以进行下面的按正常的异步并发执行。 image.png
  • 我们看下数组线程安全问题

image.png 在多线程中都对数组进行操作,我们知道赋值过程中会对旧值release新值retain。之前setter方法底层实现有过探讨。这个时候因为异步操作,多个线程不断的对数组进行releaseretain,如果同一时间2次release就自动销毁了,所以报这个错。

image.png 添加异步栅栏函数,相当于添加了一个锁,同一时间只有一个线程对数组操作,起到了同步锁的作用

image.png

2.2 原理分析

2.2.1 异步栅栏函数

异步函数: image.png 异步栅栏函数: image.png 只有flag不同具体流程可以可以参考之前的异步函数

image.png 当前队列中还有其它任务进行dx_wakeup操作,之后进行当前栅栏函数回调_dispatch_lane_barrier_complete

  • 我们把自定义队列换成全局队列,没有堵住按正常的异步函数执行。 image.png 看下官方描述

image.png 全局队列系统也会使用,或者在别的地方也会使用。如果对全局队列生效的话,就会堵塞其它的全局队列中的线程。

2.2.2 同步栅栏函数

image.png 继续_dispatch_barrier_sync_f->_dispatch_barrier_sync_f_inline

image.png 流程和同步函数类似。获取线程id检查死锁情况,执行回调。 查看:_dispatch_lane_barrier_sync_invoke_and_complete image.png

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的使用

image.png 每次只会执行一个线程的回调,我们可以把semaphore钥匙,添加了dispatch_semaphore_wait就相当于一扇门。一开始我们默认0个钥匙可以执行的。任务2添加了一道门,任务3也是。任务1执行完把钥匙传下去,有肯能传到2也可能传到3,因为是异步函数

image.png 想要实现同步也很简单dispatch_semaphore_wait放到GCD外部就可以了,相当于同步函数。

image.png

3.2 dispatch_semaphore底层分析

dispatch_semaphore_t本质是dispatch_semaphore_s其中dsema_value代表当前信号量,dsema_orig表示初始信号量。

image.png

  • 查看dispatch_semaphore_createimage.png 主要初始化一个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

image.png os_atomic_dec2o对信号量做-1操作,之后信号量大于等于0,就继续执行执行,不需要做操作;不满足的话执行_dispatch_semaphore_wait_slow

image.png 等待操作主要分为2种。1:dispatch_time_t的类型是DISPATCH_TIME_NOW则获取当前信号量小于0的话,返回超时信号并把信号量+1;2:dispatch_time_t的类型是DISPATCH_TIME_FOREVER,则一直等待调用_dispatch_sema4_wait

  • 查看dispatch_semaphore_signal

image.png 信号量+1,信号量大于0什么也不做,继续执行。否则执行_dispatch_semaphore_signal_slow

image.png 进行唤醒线程操作,如果唤醒线程则返回非0,否则返回0继续查看_dispatch_sema4_signal

image.png

3.3 信号量总结

使用信号量可以让我们优雅的控制一些异步操作的前后顺序。比如我们请求大量的图片,请求完成后做操作。1:我们可以使用串行队列中执行

4. 调度组

dispatch_group常常用来同步多个任务(注意和dispatch_barrier_sync不同的是它可以是多个队列的同步),所以其实上面先分析dispatch_semaphore也是这个原因,它本身是依靠信号量来完成的同步管理。典型的用法如下
enter-leaveimage.png dispatch_asyncimage.png

4.1 使用分析

enter-leave例子中,我们把enter和leave放入异步函数中就没有了同步的效果。

image.png 如上图所示3个异步函数执行从上到下,异步的导致执行顺序不是一定的。此时对于group的信号量是0是平衡的,所以会直接执行

image.png 值得注意的是注意enter和levae的搭配

image.png enter多一次没关系,但是levae多一次的话会报错。

image.png

4.2 dispatch_group原理分析

dispatch_group_t本质是dispatch_group_s的结构体

typedef struct dispatch_group_s *dispatch_group_t;

image.png

  • dispatch_group_create

image.png 继续

image.png 创建group对象,n指定为0,给group赋值,指定默认队列

  • dispatch_group_enter

image.png 主要是os_atomic_sub_orig2odg_bits进行-1操作

  • dispatch_group_leave

image.png 主要是os_atomic_sub_orig2odg_bits进行+1操作;根据状态,do-while循环,唤醒执行block任务;多次调用leave会导致崩溃。
查看_dispatch_group_wake

image.png 如果有notify等待则执行notify遍历并且在对应队列中执行,如果有wait任务则唤醒其执行任务

  • dispatch_group_async

image.png 和异步函数类似,继续看_dispatch_continuation_group_async

image.png 进组操作,_dispatch_continuation_async前面已经介绍过。既然有enter,必然要leave。猜测执行完notify的block后进行leave操作。

image.png 搜素_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke

image.png

  • dispatch_group_notify 如果old_state等于0,就可以进行释放了 image.png 除了leave可以进行唤醒操作,notify也可以唤醒操作。简单的说就是dispatch_group_asyncdispatch_group_notify本身就是和dispatch_group_enterdispatch_group_leave没有本质区别,后者相对更加灵活。
  • dispatch_group_wait

image.png os_atomic_rmw_loop2o不断遍历,如果(old_state & DISPATCH_GROUP_VALUE_MASK) == 0表示执行完,直接返回0;如果当前如果超时立即返回;其他情况调用_dispatch_group_wait_slow

image.png 实际使用

image.png

5. 定时器&延时操作

5.1 使用

5.1.1 主线程添加定时器

image.png 子线程添加定时器要手动开启 [[NSRunLoop currentRunLoop]run],主线程runloop是自动开启的。
2. GCD使用定时器 image.png 使用过程注意的点:

  • 定时期手动开启,dispatch_resume,暂停:dispatch_suspend。销毁:dispatch_source_cancel,开启和暂停,开启和销毁最好时配对出现,比如连续调用2次dispatch_resume,代码中的if分支不成立,执行DISPATCH_CLIENT_CRASH造成崩溃。

image.png

  • 定时器要被当前对象持有,否则会被释放depose导致无法运行,最好使用属性修饰长久持有。
@property (nonatomic, weak) dispatch_source_t timer1

image.png

  • 定时器最好时采用使用懒加载创建定时器,并且记录当timer 处于dispatch_suspend的状态。这些时候,只要在 调用dealloc 时判断下,已经调用过 dispatch_suspend 则再调用下 dispatch_resume后再cancel,然后再释放timer

  • block中注意相互持有,循环引用导致内存泄露,无法释放。

5.1.2. 延时调用

image.png 主要2种 performdispatch_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

image.png 设置定时器源的起始时间间隔时间偏差值。如果计时器源已经被取消,调用这个函数没有效果

  • 查看dispatch_source_set_event_handler image.png

查看_dispatch_source_handler_alloc回调函数 image.png 设置闭包形式的处理器,该回调函数或闭包就是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并没有明显区别:

image.png 而和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上的一幅图表示如下:

image.png GCD队列和线程原理和应用大致就探索完了,有不对的地方还望见谅。