這個問題之前也有看到,正好這兩天看到一篇文章提到這個文藝俊扳,就深入的研究了一下,地址我的同事金司機(jī)出的 5 道 iOS 多線程“面試題”,其中第一題和第二題就是考察主線程和主隊列區(qū)別的觉至。
問題
第一題(主線程只會執(zhí)行主隊列的任務(wù)嗎?)
let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
debugPrint("main thread: \(Thread.isMainThread)")
let value = DispatchQueue.getSpecific(key: key)
debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().sync(execute: log)
RunLoop.current.run()
執(zhí)行結(jié)果是什么睡腿?
第二題(主隊列任務(wù)只會在主線程上執(zhí)行嗎)
let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
debugPrint("main thread: \(Thread.isMainThread)")
let value = DispatchQueue.getSpecific(key: key)
debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().async {
DispatchQueue.main.async(execute: log)
}
dispatchMain()
執(zhí)行結(jié)果是什么语御?
解答
第一題
結(jié)果:
"main thread: true"
"main queue: false"
我們可以看swift-corelibs-libdispatch的一個PR10.6.2: Always run dispatch_sync blocks on the current thread to bett…
static void
_dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
// It's preferred to execute synchronous blocks on the current thread
// due to thread-local side effects, garbage collection, etc. However,
// blocks submitted to the main thread MUST be run on the main thread
struct dispatch_barrier_sync_slow2_s dbss2 = {
.dbss2_dq = dq,
.dbss2_func = func,
.dbss2_ctxt = ctxt,
.dbss2_ctxt = ctxt,
.dbss2_sema = _dispatch_get_thread_semaphore(),
};
struct dispatch_barrier_sync_slow_s {
@@ -746,17 +759,17 @@ _dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function
.dc_func = _dispatch_barrier_sync_f_slow_invoke,
.dc_ctxt = &dbss2,
};
dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
_dispatch_queue_push(dq, (void *)&dbss);
dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
while (dispatch_semaphore_wait(dbss2.dbss2_sema, dispatch_time(0, 3ull * NSEC_PER_SEC))) {
if (DISPATCH_OBJECT_SUSPENDED(dq)) {
continue;
}
if (_dispatch_queue_trylock(dq)) {
_dispatch_queue_drain(dq);
_dispatch_queue_unlock(dq);
}
if (dq != dispatch_get_main_queue()) {
_dispatch_thread_setspecific(dispatch_queue_key, dq);
func(ctxt);
_dispatch_workitem_inc();
_dispatch_thread_setspecific(dispatch_queue_key, old_dq);
dispatch_resume(dq);
}
_dispatch_put_thread_semaphore(dbss2.dbss2_sema);
}
It's preferred to execute synchronous blocks on the current thread
due to thread-local side effects, garbage collection, etc.
DispatchQueue.global().sync會阻塞當(dāng)前線程MainThread
峻贮,那加入DispatchQueue.global
的任務(wù)會在哪個線程執(zhí)行呢?
蘋果的解釋是為了性能应闯,因?yàn)榫€程切換是好性能的纤控,在當(dāng)前線程MainThread
中執(zhí)行任務(wù)。下面這一部分會介紹一下到底是怎樣線程切換性能的原因碉纺,內(nèi)容主要來自于
歐陽大哥深入iOS系統(tǒng)底層之CPU寄存器介紹一文嚼黔,這篇文章寫的非常好,個人很是喜愛惜辑,其中有這么一段:
線程切換時的寄存器復(fù)用
我們的代碼并不是只在單線程中執(zhí)行唬涧,而是可能在多個線程中執(zhí)行。那么這里你就可能會產(chǎn)生一個疑問盛撑?既然進(jìn)程中有多個線程在并行執(zhí)行碎节,而CPU中的寄存器又只有那么一套,如果不加處理豈不會產(chǎn)生數(shù)據(jù)錯亂的場景抵卫?答案是否定的狮荔。我們知道線程是一個進(jìn)程中的執(zhí)行單元,每個線程的調(diào)度執(zhí)行其實(shí)都是通過操作系統(tǒng)來完成介粘。也就是說哪個線程占有CPU執(zhí)行以及執(zhí)行多久都是由操作系統(tǒng)控制的殖氏。具體的實(shí)現(xiàn)是每創(chuàng)建一個線程時都會為這線程創(chuàng)建一個數(shù)據(jù)結(jié)構(gòu)來保存這個線程的信息,我們稱這個數(shù)據(jù)結(jié)構(gòu)為線程上下文姻采,每個線程的上下文中有一部分?jǐn)?shù)據(jù)是用來保存當(dāng)前所有寄存器的副本雅采。每當(dāng)操作系統(tǒng)暫停一個線程時,就會將CPU中的所有寄存器的當(dāng)前內(nèi)容都保存到線程上下文數(shù)據(jù)結(jié)構(gòu)中慨亲。而操作系統(tǒng)要讓另外一個線程執(zhí)行時則將要執(zhí)行的線程的上下文中保存的所有寄存器的內(nèi)容再寫回到CPU中婚瓜,并將要運(yùn)行的線程中上次保存暫停的指令也賦值給CPU的指令寄存器,并讓新線程再次執(zhí)行刑棵“涂蹋可以看出操作系統(tǒng)正是通過這種機(jī)制保證了即使是多線程運(yùn)行時也不會導(dǎo)致寄存器的內(nèi)容發(fā)生錯亂的問題。因?yàn)槊慨?dāng)線程切換時操作系統(tǒng)都幫它們將數(shù)據(jù)處理好了蛉签。下面的部分線程上下文結(jié)構(gòu)正是指定了所有寄存器信息的部分:
//這個結(jié)構(gòu)是linux在arm32CPU上的線程上下文結(jié)構(gòu)胡陪,代碼來自于:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h
//這里并沒有保存所有的寄存器,是因?yàn)锳BI中定義linux在arm上運(yùn)行時所使用的寄存器并不是全體寄存器碍舍,所以只需要保存規(guī)定的寄存器的內(nèi)容即可柠座。這里并不是所有的CPU所保存的內(nèi)容都是一致的,保存的內(nèi)容會根據(jù)CPU架構(gòu)的差異而不同乒验。
//因?yàn)閕OS的內(nèi)核并未開源所以無法得到iOS定義的線程上下文結(jié)構(gòu)愚隧。
//線程切換時要保存的CPU寄存器蒂阱,
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};
//線程上下文結(jié)構(gòu)
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8))); /*浮點(diǎn)寄存器*/
union vfp_state vfpstate; /*向量浮點(diǎn)寄存器*/
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
最后引申出個很經(jīng)典的問題锻全,就是蘋果的MapKit / VektorKit
狂塘,它在底層實(shí)現(xiàn)的時候,不僅僅要求代碼執(zhí)行在主線程上鳄厌,還要求執(zhí)行在 GCD 的主隊列上荞胡。所以只是在執(zhí)行的時候判斷當(dāng)前是不是主線程是不夠的,需要判斷當(dāng)前是不是在主隊列上了嚎,那怎么判斷呢?
GCD沒有提供API來進(jìn)行判斷當(dāng)前執(zhí)行任務(wù)是在什么隊列,但是我們可以利用dispatch_queue_set_specific
和 dispatch_get_specific
這一組方法為主隊列打上標(biāo)記娘香,這里是RxSwift判斷是否是主隊列的代碼:
extension DispatchQueue {
private static var token: DispatchSpecificKey<()> = {
let key = DispatchSpecificKey<()>()
DispatchQueue.main.setSpecific(key: key, value: ())
return key
}()
static var isMain: Bool {
return DispatchQueue.getSpecific(key: token) != nil
}
}
第二題
結(jié)果:
"main thread: false"
"main queue: true"
當(dāng)我把dispatchMain()
刪掉之后打印出來的結(jié)果是這樣
"main thread: true"
"main queue: true"
所以可以肯定是dispatchMain()
在作怪继阻。
之后我再加這段代碼
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("-----")
}
打印了這些
"main thread: false"
"main queue: true"
2018-08-23 14:13:19.281103+0800 MainThread[2720:5940476] [general] Attempting to wake up main runloop, but the main thread as exited. This message will only log once. Break on _CFRunLoopError_MainThreadHasExited to debug.
同時Google到這些解釋
You don't generally want to use dispatch_main(). It's for things other than regular applications (system daemons and such). It is, in fact, guaranteed to break your program if you call it in a regular app.
dispatch_main() is not for running things on the main thread — it runs the GCD block dispatcher. In a normal app, you won't need or want to use it.
還查到這個是OSX服務(wù)程序使用,iOS不使用呐伞。通過上面的解釋我猜測主隊列任務(wù)通常是在主線程執(zhí)行敌卓,但是當(dāng)遇到這種主線程已經(jīng)退出的情形,比如執(zhí)行了dispatchMain()
伶氢,蘋果在底層選擇讓其他線程來執(zhí)行主線程的任務(wù)趟径。
在看蘋果源碼看到這一段swift-corelibs-libdispatch
void
dispatch_barrier_sync(dispatch_queue_t dq, void (^work)(void))
{
// Blocks submitted to the main queue MUST be run on the main thread,
// therefore we must Block_copy in order to notify the thread-local
// garbage collector that the objects are transferring to the main thread
if (dq == dispatch_get_main_queue()) {
dispatch_block_t block = Block_copy(work);
return dispatch_barrier_sync_f(dq, block, _dispatch_call_block_and_release);
}
struct Block_basic *bb = (void *)work;
dispatch_barrier_sync_f(dq, work, (dispatch_function_t)bb->Block_invoke);
Blocks submitted to the main queue MUST be run on the main thread
雖然不夠嚴(yán)謹(jǐn),但在iOS系統(tǒng)上可以說主隊列任務(wù)只會在主線程上執(zhí)行
參考文章
深入iOS系統(tǒng)底層之CPU寄存器介紹
深入淺出GCD之dispatch_queue
深入理解 GCD
GCD's Main Queue vs. Main Thread
奇怪的GCD