想尝试写一个面试回答的系列,这是第一篇。
正文
GCD 中的 dispatch_once 在面试中是一个比较高频的出现的考察点,这篇文章以面试的角度来回答,为什么 dispatch_once 可以保证只执行一次。
这是一道面试真题,面试官提出了一个问题:
dispatch_once 为什么可以保证只执行一次?
dispatch_once_f实现原理是什么样的?dispatch_once中的原子性操作是怎样的?vval代表什么?DISPATCH_ONCE_DONE又表示什么?- 和
@synchronized的优劣分析?
我们以 Q & A 问答的形式来回答面试官的这个问题。
为什么可以保证只执行一次
Q: dispatch_once 为什么可以保证只执行一次?
A: dispatch_once 封装并执行了 dispatch_once_f 函数,其内部使用原子性操作进行标记,以此来配合信号量来决定是否唤醒其他等待的线程,而信号量则用来确保同一时间只有一个线程可以执行回调。
dispatch_once_f 实现原理是什么样的?
为了便于理解,先放上 dispatch_once_f 的源码
1 | // Block 数据结构 |
Q: dispatch_once_f 实现原理是什么样的?
A: 其内部定义了多个 _dispatch_once_waiter_s 结构体和一个 _dispatch_thread_semaphore_t 信号量,通过原子性操作 dispatch_atomic_cmpxchg 来判断标记值 vval 是否为 NULL (首次调用 dispatch_once 时,因为外部传入的 dispatch_once_t 变量值为 nil,所以 vval 会为NULL) ,如果为 NULL,则调用 _dispatch_client_callout 来执行回调,然后在回调执行完成之后,将 vval 的值更新成 DISPATCH_ONCE_DONE (表示任务已完成),最后,对链表的节点进行遍历,并调用 _dispatch_thread_semaphore_signal 来唤醒等待中的信号量。
因为dispatch_atomic_cmpxchg是原子性操作,所以只有一个线程进入到该逻辑分支中,其他线程会进入另一个分支。
如果不为 NULL 或其他线程同时也调用 dispatch_once 时,会判断回调是否 已标记完成 ,如果已完成则跳出循环;否则就是更新链表并调用 _dispatch_thread_semaphore_wait 阻塞线程,等待回调被标记完成后,再唤醒当前等待的线程。
dispatch_once 中的原子性操作是怎样的?
Q: dispatch_once 中的原子性操作是怎样的?
A: 原子性操作是 dispatch_atomic_cmpxchg(vval, NULL, &dow, acquire) ,会将 $dow 赋值给 vval ,如果 vval 的初始值为NULL,返回 YES ,否则返回 NO 。以及dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE) 将 vval 修改为指定状态 DISPATCH_ONCE_DONE。
vval 代表什么? DISPATCH_ONCE_DONE 又表示什么?
Q: vval 代表什么? DISPATCH_ONCE_DONE 又表示什么?
A: vval 可以理解为标记值, DISPATCH_ONCE_DONE 用来标记回调是否已完成,以此来决定是否要唤起信号量来解除线程的阻塞。
和 @synchronized 的优劣分析?
Q: 和 @synchronized 的优劣分析?
A: 相比之下 dispatch_once 的性能更高,速度更快,并且针对处理器进行了优化。两者分别利用来不同的方式来保证线程安全, @synchronized 采用的是递归互斥锁的方式来保证线程安全,而 dispatch_once 是使用原子操作来代替锁,使用信号量来保证线程同步。