iOS主線程和主隊列的區(qū)別

這個問題之前也有看到,正好這兩天看到一篇文章提到這個文藝俊扳,就深入的研究了一下,地址我的同事金司機(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
};


1432482-45c252046d51cb9e.png

最后引申出個很經(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_specificdispatch_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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末癣防,一起剝皮案震驚了整個濱河市蜗巧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蕾盯,老刑警劉巖幕屹,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異级遭,居然都是意外死亡香嗓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門装畅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來靠娱,“玉大人,你說我怎么就攤上這事掠兄∠裨疲” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵蚂夕,是天一觀的道長迅诬。 經(jīng)常有香客問我,道長婿牍,這世上最難降的妖魔是什么侈贷? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮等脂,結(jié)果婚禮上俏蛮,老公的妹妹穿的比我還像新娘撑蚌。我一直安慰自己,他們只是感情好搏屑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布争涌。 她就那樣靜靜地躺著,像睡著了一般辣恋。 火紅的嫁衣襯著肌膚如雪亮垫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天伟骨,我揣著相機(jī)與錄音饮潦,去河邊找鬼。 笑死携狭,一個胖子當(dāng)著我的面吹牛害晦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播暑中,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼壹瘟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鳄逾?” 一聲冷哼從身側(cè)響起稻轨,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎雕凹,沒想到半個月后殴俱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枚抵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年线欲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汽摹。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡李丰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逼泣,到底是詐尸還是另有隱情趴泌,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布拉庶,位于F島的核電站嗜憔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氏仗。R本人自食惡果不足惜吉捶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呐舔,春花似錦币励、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砌们。三九已至杆麸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浪感,已是汗流浹背昔头。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留影兽,地道東北人揭斧。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像峻堰,于是被迫代替她去往敵國和親讹开。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • iOS多線程編程 基本知識 1. 進(jìn)程(process) 進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個應(yīng)用程序捐名,就是一段程序的執(zhí)...
    陵無山閱讀 6,005評論 1 14
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • 本文首發(fā)于我的個人博客:「程序員充電站」[https://itcharge.cn]文章鏈接:「傳送門」[https...
    ITCharge閱讀 347,299評論 308 1,926
  • 定義 什么是Active Record模式 一個模型類對應(yīng)關(guān)系型數(shù)據(jù)庫中的一個表旦万,而模型類的一個實(shí)例對應(yīng)表中的一行...
    三斤牛肉閱讀 858評論 0 0
  • 昨天晚上《21天影響力倍增的人際溝通術(shù)》開課了贺归,主講是新精英的資深生涯咨詢師王奕霖老師淆两,一個多小時的線上課程精彩紛...
    了凡工作室閱讀 515評論 1 0