这是我参与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
得来的,而floo
r是之前传过来的参数
。
这里的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
,在下次进来时,就不会在执行,会直接返回- 多线程影响:如果在当前任务执行期间,有其他任务进来,会进入
无限次等待
,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的
。