iOS成长之路ios

OC底层知识点之-多线程(四)GCD下篇

2021-08-16  本文已影响0人  iOS_子矜

单例

说起单例,我们一般使用GCD的dispath_once来创建单例

对于单例,需要知道以下两个问题:

下面我们来探究一下

单例为什么只执行一次

再进入dispatch_once的源码前,我们先看下dispatch_once的参数

我们看到会调用dispatch_once_f,其中val是外界传入onceToken静态变量,而func是_dispatch_Block_invoke(block),我们看下dispatch_once_f的底层实现

通过上面代码,可以知道底层主要分为以下几步

单例block是什么时候调用

上面我们知道func就是任务block,而处理func的方法就是_dispatch_once_callout,前面判断_dispatch_once_gate_tryenter解锁,我们看下_dispatch_once_gate_tryenter这个方法实现

其源码主要是通过底层的os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识置为DLOCK_ONCE_UNLOCKED。 我们下面看下_dispatch_once_callout方法源码

上面方法主要分两步:

先看下_dispatch_client_callout方法实现

_dispatch_client_callout主要执行回调,其中f就是传入的_dispatch_Block_invoke(block),即异步回调

再看下_dispatch_once_gate_broadcast方法实现

进入 _dispatch_once_gate_broadcast -> _dispatch_once_mark_done源码,主要就是给dgo->dgo_once一个值,然后将任务的标识符为DLOCK_ONCE_DONE,即解锁

单例总结

上面我们对单例进行了探索,解开了上面所提出的问题。下面总结一下:

栅栏函数

GCD中我们有时候会用到栅栏函数来确定任务顺序,栅栏任务主要有两种

栅栏函数最直接的作用就是控制任务执行顺序保证任务按计划顺序执行

栅栏函数有几下几点需要注意:

异步栅栏函数

通过打印我们知道异步栅栏函数不会阻塞主线程堵塞的是异步函数对列

同步栅栏函数

同步栅栏函数会堵塞主线程,也会堵塞当前线程

栅栏函数总结

使用场景

栅栏函数除了用于控制任务的执行顺序,还可以用于数据安全

崩溃原因:数据不断的retain和release,在数据还没有retain完毕时,已经开始了realse,相当于对一个空数据,进行realse

下面我们添加栅栏函数

奔溃原因和上面一样,原因是栅栏函数对系统的全局队列也会阻塞,而系统其他地方也会用到全局队列,此时就会崩溃

这样就没有任何问题

除了使用栅栏函数,还可以使用互斥锁@synchronized (self) {}

之所以使用self,是因为self的生命周期大于i和mArray,这样就保证synchronized不会关联一个被销毁的对象。但是慎用@synchronized(self),这种方式很粗糙容易导致死锁

栅栏函数注意问题

异步栅栏函数 底层分析

进入dispatch_barrier_async源码实现,其底层的实现与dispatch_async类似,这里就不再做分析了,有兴趣的可以自行探索下

同步栅栏函数底层分析

进入dispatch_barrier_sync源码,实现如下

dispatch_barrier_sync调用_dispatch_barrier_sync_f,而后调用_dispatch_barrier_sync_f_inline源码。

下面我们看下_dispatch_barrier_sync_f_inline方法实现

方法实现分以下几步:

下面看下_dispatch_queue_try_acquire_barrier_sync实现

通过源码我们发现进入_dispatch_queue_try_acquire_barrier_sync_and_suspend,然后在这里进行释放 回到_dispatch_barrier_sync_f_inline方法,看1791行:_dispatch_sync_recurse方法

通过上面我们知道:

再回到_dispatch_barrier_sync_f_inline方法,看1795行:_dispatch_lane_barrier_sync_invoke_and_complete实现

信号量

信号量的作用一般是用来使任务同步执行,类似于互斥锁,用户可以根据需要控制GCD最大并发数,一般是这样使用的

dispatch_semaphore_create 创建

该函数的底层实现如下,主要是用来初始化信号量,并设置GCD的最大并发数其最大并发数必须大于0

dispatch_semaphore_wait 加锁

该函数的源码实现看到,其主要作用是对信号量dsema通过os_atomic_dec2o进行了--操作,其内部是执行的C++的atomic_fetch_sub_explicit方法。

将具体的值带入为

os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value - 1

_dispatch_semaphore_wait_slow

进入_dispatch_semaphore_wait_slow的源码实现,当value小于0时,根据等待事件timeout做出不同操作

dispatch_semaphore_signal 解锁

该函数的源码实现可以知道,其核心也是通过os_atomic_inc2o函数value进行了++操作os_atomic_inc2o内部是通过C++的atomic_fetch_add_explicit

其中os_atomic_dec2o的宏定义转换如下

将具体的值带入:

os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m) 
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value + 1

信号量总结

调度组(线程组)

线程组使用

调度组的最直接作用是控制任务执行顺序,常见的方式如下

dispatch_group_create 创建组 
dispatch_group_async 进组任务 
dispatch_group_notify 进组任务执行完毕通知 dispatch_group_wait 进组任务执行等待时间

//进组和出组需要是成对使用的,不然会有问题
dispatch_group_enter 进组 
dispatch_group_leave 出组
我们看下如何使用

将dispatch_group_notify移到最前面

通过上面三图我们可以知道,dispatch_group_notify前移会导致调度组失效,第三图和第四图可以知道,dispatch_group_enter可以单独存在,而dispatch_group_leave必须和dispatch_group_enter成对出现,否则报错,报错有延迟是因为async是并发,会有延迟

多加一个dispatch_group_enter

此时不会执行notify,原因是少了一个leave,会让notify一直等待

底层源码

dispatch_group_create 创建组

作用:创建group,并设置属性,此时的group的value为0

查看dispatch_group_create源码

上面方法执行:dispatch_group_create->_dispatch_group_create_with_count,其中_dispatch_group_create_with_count是对group对象进行赋值,并返回group对象,其中n值为0。

dispatch_group_enter 进组

看下dispatch_group_enter

通过os_atomic_sub_orig2o对dg->dg.bits 作 --操作,对数值进行处理

dispatch_group_leave 出组

看下dispatch_group_leave源码

源码进行如下操作

下面进入_dispatch_group_wake源码

执行过程:

下面再看下_dispatch_continuation_async源码

这步与异步函数的block回调执行时一致的,不过多解释

dispatch_group_notify 通知

查看dispatch_group_notify源码实现

通过上面我们知道:如果old_state等于0,就可以进行释放了,除了leave可以通过_dispatch_group_wake唤醒,其中dispatch_group_notify也可以唤醒的。

其中os_mpsc_push_update_tail是宏定义,用于获取dg的状态码

dispatch_group_async

查看dispatch_group_async源码

可以看到dispatch_group_async方法主要做了两件事:

再看下_dispatch_continuation_group_async源码

方法主要封装了dispatch_group_enter进组操作,之后调用_dispatch_continuation_async方法,这个方法在执行leave中的_dispatch_group_wake方法里也调用了。都是进行常规的异步函数底层操作

猜想:上面我们知道enter和leave是成对出现,所以block执行之后可能隐性的执行leave,通过断点调试,打印堆栈信息

通过堆栈信息,我们看到执行_dispatch_client_callout后执行销毁方法_dispatch_call_block_and_release。我们看下_dispatch_client_callout源码

完美印证dispatch_group_async底层调用了enter-leave

调度组总结

dispatch_source

dispatch_source 定义

定义dispatch_source基础数据类型,用于协调特定底层系统事件的处理,其CPU负荷较小,占用很少资源,具有联结优势

dispatch_source替代了异步回调函数,来处理系统相关的事件,当配置一个dispatch时,你需要指定监测的事件dispatch queue、以及处理事件的代码(block或函数)。当事件发生时,dispatch source会提交你的block或函数指定的queue去执行

使用 Dispatch Source不使用 dispatch_async 的唯一原因就是利用联结的优势

dispatch_source流程

在任一线程上调用它的一个函数dispatch_source_merge_data后,会执行Dispatch Source事先定义好的句柄(可以把句柄简单理解为一个block),这个过程叫Custom event,用户事件是dispatch source支持处理的一种事件。

简单来说就是:事件是由你调用 dispatch_source_merge_data 函数来向自己发出的信号。

句柄指向指针的指针,它指向的局势一个类或者结构,它和系统有密切的关系,这当中还有通用句柄,就是HANDLE。它有一下几类

使用

创建dispatch

Dispatch Source 种类

其中type的类型有一下几种:

type(种类) 说明
DISPATCH_SOURCE_TYPE_DATA_ADD 自定义的事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 自定义的事件,变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力 (注:iOS8后可用)
DISPATCH_SOURCE_TYPE_PROC 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL 接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应

上面说了不少类型,我们需要注意两个类型:

常用函数

//挂起队列
dispatch_suspend(queue) 

//分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复
dispatch_resume(source) 

//向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。
dispatch_source_merge_data 

//设置响应分派源事件的block,在分派源指定的队列上运行
dispatch_source_set_event_handler 

//得到分派源的数据
dispatch_source_get_data 

//得到dispatch源创建,即调用dispatch_source_create的第二个参数
uintptr_t dispatch_source_get_handle(dispatch_source_t source); 

//得到dispatch源创建,即调用dispatch_source_create的第三个参数
unsigned long dispatch_source_get_mask(dispatch_source_t source); 

//取消dispatch源的事件处理--即不再调用block。如果调用dispatch_suspend只是暂停dispatch源。
void dispatch_source_cancel(dispatch_source_t source); 

//检测是否dispatch源被取消,如果返回非0值则表明dispatch源已经被取消
long dispatch_source_testcancel(dispatch_source_t source); 

//dispatch源取消时调用的block,一般用于关闭文件或socket等,释放相关资源
void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t cancel_handler); 

//可用于设置dispatch源启动时调用block,调用完成后即释放这个block。也可在dispatch源运行当中随时调用这个函数。
void dispatch_source_set_registration_handler(dispatch_source_t source, dispatch_block_t registration_handler); 

使用场景

经常用于验证码倒计时,因为dispatch_source不依赖于Runloop,而是直接和底层内核交互准确性更高

写到最后

文章我们分析了单例,栅栏函数,信号量,调度组,以及dispatch_source,主要对单例,栅栏函数,信号量,调度组的实现以及查看了其实现的底层原理。线程的源码比较难理解,有兴趣的可以去官方下载源码,自己操作理解一下。内容比较多,有些地方没有详细的去说明,有不严谨的地方希望各位指出!最近分析锁的底层实现,有时间会写出来

收录

上一篇 下一篇

猜你喜欢

热点阅读