单例
说起单例,我们一般使用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_DON
E,表示任务执行过
了,只接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_once_gate_broadcast方法实现_dispatch_client_callout主要执行回调,其中f就是传入的_dispatch_Block_invoke(block),即异步回调
进入 _dispatch_once_gate_broadcast -> _dispatch_once_mark_done源码,主要就是
给dgo->dgo_once一个值
,然后将任务的标识符为DLOCK_ONCE_DONE
,即解锁
。
单例总结
上面我们对单例进行了探索,解开了上面所提出的问题。下面总结一下:
- 1.【单例执行一次原理】:GCD单例中,有两个重要参数,
onceToken
和block
,其中onceToken是静态变量
,具有唯一性
,在底层被封装成了dispatch_once_gate_t类型的变量l
,l主要是用来获取底层原子封装性的关联,即变量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_f_inline方法实现dispatch_barrier_sync调用_dispatch_barrier_sync_f,而后调用_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
对向前信息进行处理
。
信号量
信号量的作用一般是用来使任务同步执行
,类似于互斥锁
,用户可以根据需要控制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多次调用,会崩溃
执行过程:
- 1.do-while循环进行
异步
命中 - 2.
_dispatch_continuation_async执行任务
- 3.
_dispatch_wake_by_address开始地址释放
- 4.
_dispatch_release_n引用释放
这步与
异步函数的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_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_notif
y在底层主要是判断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
替代了异步回调函数
,来处理系统相关的事件
,当配置一个dispatc
h时,你需要指定监测的事件
、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
,主要对单例,栅栏函数,信号量,调度组的实现以及查看了其实现的底层原理。线程的源码比较难理解,有兴趣的可以去官方下载源码,自己操作理解一下。内容比较多,有些地方没有详细的去说明,有不严谨的地方希望各位指出!最近分析锁的底层实现,有时间会写出来
网友评论