美文网首页iOS成长之路ios
OC底层知识点之-多线程(四)GCD下篇

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

作者: iOS_子矜 | 来源:发表于2021-08-16 21:54 被阅读0次

    单例

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

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

    • 1.单例为什么只执行一次,底层是如何控制的
    • 2.单例的block是在什么时候进行调用

    下面我们来探究一下

    单例为什么只执行一次

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

    • 1.onceToken,这是一个静态变量,由于不同位置定义的静态变量是不同的,所以静态变量具有唯一性
    • 2.block回到

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

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

    • 1.将val,也就是静态变量转换为dispatch_once_gate_t类型变量l
    • 2.通过os_atomic_load获取此时的任务的标识符v
    • 3.如果v等于DLOCK_ONCE_DONE,表示任务执行过了,只接return
    • 4.如果任务执行后,加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,函数里再次进行存储,将标识符置为DLOCK_ONCE_DONE
    • 5.反之,则通过_dispatch_once_gate_tryenter尝试进入任务,即解锁,然后执行_dispatch_once_callout执行block回调
    • 6.如果此时有任务正在执行再有任务进来,则通过_dispatch_once_wait函数让新来的任务进入无限次等待

    单例block是什么时候调用

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

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

    上面方法主要分两步:

    • 1._dispatch_client_callout:block回调执行
    • 2._dispatch_once_gate_broadcast:进行广播
    先看下_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,即解锁

    单例总结

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

    • 1.【单例执行一次原理】:GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量ll主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return。
    • 2.【block调用时机】:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回
    • 3.【对多线程的印象】:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的。

    栅栏函数

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

    • 1.同步栅栏函数dispatch_barrier_sync(在主线程中执行):前面的任务执行完毕才会来到这里,但是同步栅栏函数会堵塞线程,影响后面的任务执行
    • 2.异步栅栏函数dispatch_barrier_async:前面的任务执行完毕才会来到这里

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

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

    • 1.栅栏函数只能控制同一并发队列
    • 2.同步栅栏添加进入队列的时候,当前线程会被锁死,直到同步栅栏之前的任务同步栅栏任务本身执行完毕时,当前线程才会打开然后继续执行下一句代码
    • 3.在使用栅栏函数时.使用自定义队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用,没有任何意义

    异步栅栏函数

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

    同步栅栏函数

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

    栅栏函数总结

    • 1.异步栅栏函数阻塞的是队列,而且必须是自定义的并发队列不影响主线程任务的执行
    • 2.同步栅栏函数阻塞的是线程,且是主线程,会影响主线程其他任务的执行

    使用场景

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

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

    下面我们添加栅栏函数

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

    这样就没有任何问题

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

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

    栅栏函数注意问题

    • 1.如果栅栏函数中使用全局队列,运行会崩溃,原因是系统也在用全局并发队列,使用栅栏同时会拦截系统的,所以会崩溃
    • 2.如果将自定义并发队列改为串行队列,即serial ,串行队列本身就是有序同步 此时加栅栏会浪费性能
    • 3.栅栏函数只会阻塞一次

    异步栅栏函数 底层分析

    进入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方法实现

    方法实现分以下几步:

    • 1.通过_dispatch_tid_self获取线程ID
    • 2.通过_dispatch_queue_try_acquire_barrier_sync判断线程状态。

    下面看下_dispatch_queue_try_acquire_barrier_sync实现

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

    通过上面我们知道:

    • 1.通过_dispatch_sync_recurse递归查找栅栏函数的target
    • 2.通过_dispatch_introspection_sync_begin向前信息进行处理
    再回到_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方法。

    • 1.如果value 大于等于0,表示操作无效,即执行成功
    • 2.如果value 等于LONG_MIN系统会抛出一个crash
    • 3.如果value 小于0,则进入长等待

    将具体的值带入为

    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

    • 1.如果value 大于 0,表示操作无效,即执行成功
    • 2.如果value 等于0,则进入长等待

    其中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
    

    信号量总结

    • 1.dispatch_semaphore_create 主要就是初始化限号量
    • 2.dispatch_semaphore_wait是对信号量的value进行--,即加锁操作
    • 3.dispatch_semaphore_signal 是对信号量的value进行++,即解锁操作

    调度组(线程组)

    线程组使用

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

    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源码

    源码进行如下操作

    • 1.-1到0,即++操作
    • 2.根据状态,do-while循环,唤醒执行block任务
    • 3.如果0 + 1 = 1,enter-leave不平衡,即leave多次调用,会崩溃
    下面进入_dispatch_group_wake源码

    执行过程:

    • 1.do-while循环进行异步命中
    • 2._dispatch_continuation_async执行任务
    • 3._dispatch_wake_by_address开始地址释放
    • 4._dispatch_release_n引用释放
    下面再看下_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方法主要做了两件事:

    • 1.包装任务
    • 2.异步处理任务
    再看下_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

    调度组总结

    • 1.enter-leave只要成对出现就可以不分前后,距离(同一作用域)
    • 2.dispatch_group_enter在底层是通过C++函数,对group的value执行--操作(即0 -> -1)
    • 3.dispatch_group_leave在底层是通过C++函数,对group的value进行++操作(即-1 -> 0)
    • 4.dispatch_group_notify在底层主要是判断group的state是否等于0,当等于0时,就通知唤醒
    • 5.block任务的唤醒,可以通过dispatch_group_leave,也可以通过dispatch_group_notify
    • 6.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。它有一下几类

    • 1.实例句柄 HINSTANCE
    • 2.位图句柄 HBITMAP
    • 3.设备表句柄 HDC
    • 4.图标句柄 HICON

    使用

    创建dispatch

    • 1.type:dispatch源可处理的事件
    • 2.handle:理解为句柄、索引或id,假如要监听进程,需要传入进程的ID
    • 3.mask:理解为描述,提供更详细的描述,让它知道具体要监听什么
    • 4.queue:自定义源需要的一个队列,用来处理所有的响应句柄

    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操作的写响应

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

    • 1.DISPATCH_SOURCE_TYPE_DATA_ADD:当同一时间一个事件的的触发频率很高,那么Dispatch Source会将这些响应ADD的方式进行累积,然后等系统空闲时最终处理,如果触发频率比较零散,那么Dispatch Source会将这些事件分别响应
    • 2.DISPATCH_SOURCE_TYPE_DATA_OR:是自定义的事件,但是它是以OR的方式进行累积

    常用函数

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

    收录

    相关文章

      网友评论

        本文标题:OC底层知识点之-多线程(四)GCD下篇

        本文链接:https://www.haomeiwen.com/subject/bbyvbltx.html