正文
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
是使用原子操作来代替锁,使用信号量来保证线程同步。