iOS主線程和主隊列的區別

 

問題

第一題(主線程只會執行主隊列的任務嗎?)

let key = DispatchSpecificKey()

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()

執行結果是什麼?

第二題(主隊列任務只會在主線程上執行嗎)

let key = DispatchSpecificKey()

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()

執行結果是什麼?

解答

第一題

結果:

"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會阻塞當前線程MainThread,那加入DispatchQueue.global的任務會在哪個線程執行呢?
蘋果的解釋是爲了性能,因爲線程切換是好性能的,在當前線程MainThread中執行任務。下面這一部分會介紹一下到底是怎樣線程切換性能的原因,內容主要來自於
歐陽大哥深入iOS系統底層之CPU寄存器介紹一文,這篇文章寫的非常好,個人很是喜愛,其中有這麼一段:

線程切換時的寄存器複用   
我們的代碼並不是只在單線程中執行,而是可能在多個線程中執行。那麼這裏你就可能會產生一個疑問?既然進程中有多個線程在並行執行,而CPU中的寄存器又只有那麼一套,如果不加處理豈不會產生數據錯亂的場景?答案是否定的。我們知道線程是一個進程中的執行單元,每個線程的調度執行其實都是通過操作系統來完成。也就是說哪個線程佔有CPU執行以及執行多久都是由操作系統控制的。具體的實現是每創建一個線程時都會爲這線程創建一個數據結構來保存這個線程的信息,我們稱這個數據結構爲線程上下文,每個線程的上下文中有一部分數據是用來保存當前所有寄存器的副本。每當操作系統暫停一個線程時,就會將CPU中的所有寄存器的當前內容都保存到線程上下文數據結構中。而操作系統要讓另外一個線程執行時則將要執行的線程的上下文中保存的所有寄存器的內容再寫回到CPU中,並將要運行的線程中上次保存暫停的指令也賦值給CPU的指令寄存器,並讓新線程再次執行。可以看出操作系統正是通過這種機制保證了即使是多線程運行時也不會導致寄存器的內容發生錯亂的問題。因爲每當線程切換時操作系統都幫它們將數據處理好了。下面的部分線程上下文結構正是指定了所有寄存器信息的部分:

//這個結構是linux在arm32CPU上的線程上下文結構,代碼來自於:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h  
//這裏並沒有保存所有的寄存器,是因爲ABI中定義linux在arm上運行時所使用的寄存器並不是全體寄存器,所以只需要保存規定的寄存器的內容即可。這裏並不是所有的CPU所保存的內容都是一致的,保存的內容會根據CPU架構的差異而不同。
//因爲iOS的內核並未開源所以無法得到iOS定義的線程上下文結構。

//線程切換時要保存的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 */
};

//線程上下文結構
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)));  /*浮點寄存器*/
    union vfp_state     vfpstate;  /*向量浮點寄存器*/
#ifdef CONFIG_ARM_THUMBEE
    unsigned long       thumbee_state;  /* ThumbEE Handler Base register */
#endif
};

最後引申出個很經典的問題,就是蘋果的MapKit / VektorKit,它在底層實現的時候,不僅僅要求代碼執行在主線程上,還要求執行在 GCD 的主隊列上。所以只是在執行的時候判斷當前是不是主線程是不夠的,需要判斷當前是不是在主隊列上,那怎麼判斷呢?
GCD沒有提供API來進行判斷當前執行任務是在什麼隊列,但是我們可以利用dispatch_queue_set_specific和 dispatch_get_specific這一組方法爲主隊列打上標記,這裏是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
    }
}

第二題

結果:

"main thread: false"
"main queue: true"

當我把dispatchMain()刪掉之後打印出來的結果是這樣

"main thread: true""main queue: true"

所以可以肯定是dispatchMain()在作怪。

之後我再加這段代碼

override func touchesBegan(_ touches: Set, 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服務程序使用,iOS不使用。通過上面的解釋我猜測主隊列任務通常是在主線程執行,但是當遇到這種主線程已經退出的情形,比如執行了dispatchMain(),蘋果在底層選擇讓其他線程來執行主線程的任務。

在看蘋果源碼看到這一段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

雖然不夠嚴謹,但在iOS系統上可以說主隊列任務只會在主線程上執行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章