这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战
1. 死锁
所谓死锁,通常指有两个线程A和B都卡住了无法完成,都在等待对方完成任务后在执行。A不能完成是因为它在等待B完成。但B也不能完成,因为它在等待A完成。于是大家都无法完成任务,就导致了死锁(DeadLock)。
2 同步/异步函数
同步,异步区别
- 能否开辟
新线程 - 任务回调是否具备
异步性 - 同步性 死锁情况的产生
2.1 同步函数
下面分析串行和并行执行同步函数的情况。
串行
上文说到同步函数实现会走到这里,这里看到dq_width == 1 的时候,也就是当是串行队列的时候,会调用_dispatch_barrier_sync_f。
搜索_dispatch_barrier_sync_f。发现调用了_dispatch_barrier_sync_f_inline。
搜索_dispatch_barrier_sync_f_inline。
当死锁的时候,都会调用_dispatch_sync_f_slow,所以这里需要看_dispatch_sync_f_slow。这里也可以知道_dispatch_sync_f_slow不报错,报错的是 __DISPATCH_WAIT_FOR_QUEUE__
进去_dispatch_sync_f_slow。
接下来看报错的地方,也就是__DISPATCH_WAIT_FOR_QUEUE__,看到其报错的地方有条件判断,那么就进去看是如何判断是死锁的。这里的2个参数一个是线程状态,一个是线程id。
这里做了中间层封装,实际调用_dispatch_lock_is_locked_by。
点进去_dispatch_lock_is_locked_by。DLOCK_OWNER_MASK是个很大的值,只要(lock_value ^ tid) 不为空,那么与上DLOCK_OWNER_MASK就不会为0。如果(lock_value ^ tid) & DLOCK_OWNER_MASK为0,那么就代表着(lock_value ^ tid)为0,那么就说明lock_value等于tid。那么就说明,本来是要调起这个队列,但是这个队列原定是要等待的,构成了一个矛盾,所以发生了死锁。
并行
在_dispatch_sync_f_inline里面还有这四个函数,并不知道并行队列是调用哪一个。这里注意到_dispatch_sync_invoke_and_complete的参数func和DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))之间没有逗号隔开,这是因为宏定义的时候,已经把逗号加上了。
打下断点后运行,发现进入了_dispatch_sync_invoke_and_complete函数。
_dispatch_sync_invoke_and_complete进去后发现调用了3个函数,分别是:_dispatch_sync_function_invoke_inline,_dispatch_trace_item_complete,_dispatch_lane_non_barrier_complete。
点击_dispatch_sync_function_invoke_inline后发现这里调用了_dispatch_client_callout,也就是block调用的地方。
2.2 异步函数
串行
上文说到,dispatch_async最终会调用dq_push,而dq_push在不同的线程下赋值是不一样的,这边来看一下串行队列时候的情况。
这里看到串行的dq_push调用的是_dispatch_lane_push。
然后来到dx_wakeup也就是dq_wakeup,查一下定义是_dispatch_lane_wakeup。
当要执行的时候,肯定是没有barrier的,所以状态为DISPATCH_WAKEUP_BARRIER_COMPLETE,会进入_dispatch_lane_barrier_complete。
当dq_items_tail不为空并且任务不是挂起的状态的时候才能执行,因为是串行,所以dq_width == 1,所以走_dispatch_lane_drain_barrier_waiter。
然后走_dispatch_barrier_waiter_redirect_or_wake。
这里会将dq的类型转为DISPATCH_QUEUE_GLOBAL_ROOT_TYPE类型,然后下面的dx_push就会调用_dispatch_root_queue_push。然后之后的步骤就和上文分析的差不多,最后调用_dispatch_client_callout调用block。
并行
并行的dq_push是_dispatch_lane_concurrent_push。
然后搜索_dispatch_lane_concurrent_push。
然后调用了_dispatch_continuation_redirect_push。这里会将dq的类型转为DISPATCH_QUEUE_GLOBAL_ROOT_TYPE类型,然后下面的dx_push就会调用_dispatch_root_queue_push。
接下来会走到_dispatch_root_queue_push。
然后走到_dispatch_root_queue_push_inline。
在走到_dispatch_root_queue_poke里面。
然后走到_dispatch_root_queue_poke_slow。看到 _dispatch_root_queues_init()。
这里进行了单例的调用,调用_dispatch_root_queues_init_once。
上文中说到这里之后调用_dispatch_worker_thread2等一系列函数,最后调用_dispatch_client_callout来调用block。那么这里的_dispatch_worker_thread2是哪里调用的呢?这里的_dispatch_worker_thread2封装给了pthread的api。GCD也是对pthread的封装。这里的调用执行,是通过workloop调用的,而不是立即执行,是受cpu进行调控处理的。
回到_dispatch_root_queue_poke_slow。如果是全局队列,那么就会创建线程去处理。
如果是普通的自己创建的线程,就会进行dowhile循环。这里dgq_thread_pool_size会暂时标记为1,这是因为正常的并行队列是0的,而全局队列为1的是因为他的线程量比正常的并行队列多一个。
上文中说到并行队列的DQF_WIDTH 为DISPATCH_QUEUE_WIDTH_MAX。
而DISPATCH_QUEUE_WIDTH_POOL为全局队列的DQF_WIDTH,并且比DISPATCH_QUEUE_WIDTH_MAX大一。
dgq_thread_pool_size会根据需求不断进行++。
回到do while 循环,看到remaining = can_request,这里的can_request = t_count < floor ? 0 : t_count - floor;,t_count是通过os_atomic_load2o得来的,而floor是之前传过来的参数。
这里的remaining 是之前传过来的参数,值为1.
remaining 一般不会大于 can_request,否则就会报异常。remaining是需要的线程数,而can_request是可以请求的线程数。这里如果大于,就会进行--的操作,如果remaining为0,那么就代表着线程池已经满了,那么就会直接return。
3. 单例
单例是通过这样的方式来创建的。
源码中搜索dispatch_once,发现实际调用的是dispatch_once_f。这里的val是外面传进来的onceToken。
这里对val进行了强制类型的转换。然后通过os_atomic_load获取标识符并赋值给V,如果标识符是DLOCK_ONCE_DONE,代表已经执行过,那么就直接return。如果 任务执行后,加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为DLOCK_ONCE_DONE。
一般来说,第一次进来的话,会走到_dispatch_once_gate_tryenter这里,而里面做了解锁的操作,是对多线程的封装处理,所以是线程安全的。最后,就调用了_dispatch_once_callout。如果加锁了,那么就会调用_dispatch_once_wait进行无限制等待开锁的状态。
这里主要是通过底层os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识符置为DLOCK_ONCE_UNLOCKED。
_dispatch_once_callout调用_dispatch_client_callout执行了block,调用_dispatch_once_gate_broadcast进行标记符的处理。
这里调用了_dispatch_once_mark_done进行标记符的处理。
这里就标记为DLOCK_ONCE_DONE了,这样下次进来的话,就会直接return了。
- 单例只执行一次的原理:GCD单例中,有两个重要参数,
onceToken和block,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量l,l主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return- block调用时机:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为
DLOCK_ONCE_UNLOCKED,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回- 多线程影响:如果在当前任务执行期间,有其他任务进来,会进入
无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的。