原文地址html
多線程一直是我至關感興趣的技術知識之一,我的尤爲喜好GCD
這個輕量級的多線程解決方案,爲了瞭解其實現,不厭其煩的翻閱libdispatch
的源碼。甚至由於太喜歡了,原本想要寫這相應的源碼解析系列文章,但懼怕寫的很差,因而除了開篇的類型介紹,也是草草了事,沒了下文swift
剛好這幾天好友出了幾道有關GCD
的題目,運行結果出於意料,仔細摸索後,發現蘋果基於libdispatch
作了一些有趣的修改工做,因而想將這兩道題目分享出來。因爲朋友提供的運行代碼爲Swift
書寫,在此我轉換成等效的OC
代碼進行講述。你若是瞭解了下面兩個概念,會讓後續的閱讀更加容易:api
對於主線程和主隊列,咱們可能會有這麼一個理解多線程
主線程只會執行主隊列的任務。一樣,主隊列只會在主線程上被執行異步
首先是主線程只會執行主隊列的任務。在iOS
中,只有主線程才擁有權限向渲染服務提交打包的圖層樹信息,完成圖形的顯示工做。而咱們在work queue
中提交的UI
更新老是無效的,甚至致使崩潰發生。而因爲主隊列只有一條,其餘的隊列所有都是work queue
,所以能夠得出主線程只會執行主隊列的任務
這一結論。可是,有下面這麼一段代碼:async
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_set_specific(mainQueue, "key", "main", NULL);
dispatch_sync(globalQueue, ^{
BOOL res1 = [NSThread isMainThread];
BOOL res2 = dispatch_get_specific("key") != NULL;
NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2);
});
複製代碼
根據正常邏輯的理解來講,這裏的兩個判斷結果應該都是NO
,但運行後,第一個判斷爲YES
,後者爲NO
,輸出說明了主線程此時執行了work queue
的任務函數
上面的代碼在換成async
以後就會獲得預期的判斷結果,但在同步執行的狀況下就會致使這個問題。在查找緣由以前,借用bestswifter
文章中的代碼一用,首先sync
的調用棧以及大體源碼以下:oop
dispatch_sync
└──dispatch_sync_f
└──_dispatch_sync_f2
└──_dispatch_sync_f_slow
static void _dispatch_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) {
_dispatch_thread_semaphore_t sema = _dispatch_get_thread_semaphore();
struct dispatch_sync_slow_s {
DISPATCH_CONTINUATION_HEADER(sync_slow);
} dss = {
.do_vtable = (void*)DISPATCH_OBJ_SYNC_SLOW_BIT,
.dc_ctxt = (void*)sema,
};
_dispatch_queue_push(dq, (void *)&dss);
_dispatch_thread_semaphore_wait(sema);
_dispatch_put_thread_semaphore(sema);
// ...
}
複製代碼
能夠看到對於libdispatch
對於同步任務的處理是採用sema
信號量的方式堵塞調用線程直到任務被處理完成,這也是爲何sync
嵌套使用是一個死鎖問題。根據源碼能夠獲得執行的流程圖:性能
但實際運行後,block
是執行在主線程上的,代碼真正流程是這樣的:學習
所以能夠作一個猜測:
因爲
sync
函數自己會堵塞當前執行線程直到任務執行。爲了減小線程切換的開銷,以及避免線程被堵塞的資源浪費,因而對sync
函數進行了改進:在大多數狀況下,直接在當前線程執行同步任務
既然有了猜測,就須要驗證。之因此說是大多數狀況,是由於目前主隊列只在主線程上被執行
仍是有效的,所以咱們排除global -sync-> main
這種條件。所以爲了驗證效果,須要建立一個串行線程:
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_sync(globalQueue, ^{
BOOL res1 = [NSThread isMainThread];
BOOL res2 = dispatch_get_specific("key") != NULL;
NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2);
});
dispatch_async(globalQueue, ^{
NSThread *globalThread = [NSThread currentThread];
dispatch_sync(serialQueue, ^{
BOOL res = [NSThread currentThread] == globalThread;
NSLog(@"is same thread: %zd", res);
});
});
複製代碼
運行後,兩次判斷的結果都是YES
,結果足以驗證猜測,能夠肯定蘋果爲了提升性能,已經對sync
作了修改。另外global -sync-> main
測試結果發現sync
的調用過程不會被優化
上面說過,只有主線程纔有權限提交渲染任務。一樣的,出於下面兩個設定,這個理解應當是成立的:
UIKit
的接口api
一樣的,朋友給出了另外一份代碼:
dispatch_queue_set_specific(mainQueue, "key", "main", NULL);
dispatch_block_t log = ^{
printf("main thread: %zd", [NSThread isMainThread]);
void *value = dispatch_get_specific("key");
printf("main queue: %zd", value != NULL);
}
dispatch_async(globalQueue, ^{
dispatch_async(dispatch_get_main_queue(), log);
});
dispatch_main();
複製代碼
運行以後,輸出結果分別爲NO
和YES
,也就是說此時主隊列的任務並無在主線程上執行。要弄清楚這個問題的緣由顯然難度要比上一個問題難度大得多,由於若是子線程能夠執行主隊列的任務,那麼此時是沒法提交打包圖層信息到渲染服務的
一樣的,咱們能夠先猜想緣由。不一樣於正常的項目啓動代碼,這個Swift
文件的運行更像是腳本運行,由於缺乏了一段啓動代碼:
@autoreleasepool
{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
複製代碼
爲了找到答案,首先須要對問題主線程只會執行主隊列的任務
的代碼進行改造一下。另外因爲第二個問題涉及到執行任務所在的線程
,mach_thread_self
函數會返回當前線程的id
,能夠用來判斷兩個線程是否相同:
thread_t threadId = mach_thread_self();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(globalQueue, ^{
dispatch_async(mainQueue, ^{
NSLog(@"%zd --- %zd", threadId == mach_thread_self(), [NSThread isMainThread]);
});
});
@autoreleasepool
{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
複製代碼
這段代碼的運行結果都是YES
,說明在UIApplicationMain
函數先後主隊列任務執行的線程id
是相同的,所以能夠得出兩個條件:
UIApplicationMain
函數調用後,isMainThread
返回了正確結果結合這兩個條件,能夠作出猜測:在UIApplicationMain
中存在某個操做使得本來執行主隊列任務的線程變成了主線程
,其猜測圖以下:
因爲UIApplicationMain
是個私有api
,咱們沒有其實現代碼,可是咱們都知道在這個函數調用以後,主線程的runloop
會被啓動,那麼這個線程的變更是否是跟runloop
的啓動有關呢?爲了驗證這個判斷,在手動啓動runloop
定時的去檢測線程:
dispatch_block_t log = ^{
printf("is main thread: %zd\n", [NSThread isMainThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), log);
}
dispatch_async(globalQueue, ^{
dispatch_async(dispatch_get_main_queue(), log);
});
[[NSRunLoop currentRunLoop] run];
複製代碼
在runloop
啓動後,全部的檢測結果都是YES
:
// console log
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
複製代碼
代碼的運行結果驗證了這個猜測,但結論就變成了:
thread
->runloop
->main thread
這樣的結論,隨便啓動一個work queue
的runloop
就能輕易的推翻這個結論,那麼是否可能只有第一次啓動runloop
的線程纔有可能變成主線程?爲了驗證這個猜測,繼續改造代碼:
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t logSerial = ^{
printf("is main thread: %zd\n", [NSThread isMainThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, log);
}
dispatch_async(serialQueue, ^{
[[NSRunLoop currentRunLoop] run];
});
dispatch_async(globalQueue, ^{
dispatch_async(serialQueue, logSerial);
});
dispatch_main();
複製代碼
在保證了子線程的runloop
是第一個被啓動的狀況下,全部運行的輸出結果都是NO
,也就是說由於runloop
修改了線程的priority
的猜測是不成立的,那麼基於UIApplicationMain
測試代碼的兩個條件沒法解釋主隊列爲何沒有運行在主線程上
通過來回推敲,我發現主隊列老是在同一個線程上執行
這個條件限制了進一步擴大猜測的可能性,爲了驗證這個條件,經過定時輸出主隊列任務所在的threadId
來檢測這個條件是否成立:
thread_t threadId = mach_thread_self();
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
printf("current thread id is: %d\n", threadId);
dispatch_block_t logMain = ^{
printf("=====main queue======> thread id is: %d\n", mach_thread_self());
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), logMain);
}
dispatch_block_t logSerial = ^{
printf("serial queue thread id is: %d\n", mach_thread_self());
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, logSerial);
}
dispatch_async(globalQueue, ^{
dispatch_async(serialQueue, logSerial);
dispatch_async(dispatch_get_main_queue(), logMain);
});
dispatch_main();
複製代碼
在測試代碼中增長子隊列定時作對比,發現無論是serial queue
仍是main queue
,都有可能運行在不一樣的線程上面。可是若是去掉了子隊列做爲對比,main queue
只會執行在一條線程上,但該線程的threadId
老是不等同於咱們保存下來的數值:
// console log
current thread id is: 775
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 7171"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 6403"
"serial queue thread id is: 4355"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 4355"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 4355"
"=====main queue======> thread id is: 4355"
"serial queue thread id is: 6403"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 1547"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
複製代碼
發現了這一個新的現象後,結合以前的信息來看,能夠得出一個新的猜測:
有一個專用啓動線程用於啓動主線程的
runloop
,啓動前主隊列會被這個線程執行
要測試這個猜測也很簡單,只要對比runloop
先後的threadId
是否一致就能夠了:
thread_t threadId = mach_thread_self();
printf("current thread id is: %d\n", threadId);
dispatch_block_t logMain = ^{
printf("=====main queue======> thread id is: %d\n", mach_thread_self());
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), logMain);
}
dispatch_block_t logSerial = ^{
printf("serial queue thread id is: %d\n", mach_thread_self());
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, logSerial);
}
dispatch_async(globalQueue, ^{
dispatch_async(serialQueue, logSerial);
dispatch_async(dispatch_get_main_queue(), logMain);
});
[[NSRunLoop currentRunLoop] run];
// console log
current thread id is: 775
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
"=====main queue======> thread id is: 775"
複製代碼
運行結果說明了並不存在什麼啓動線程
,一旦runloop
啓動後,主隊列就會一直執行在同一個線程上,而這個線程就是主線程。因爲runloop
自己是一個不斷循環處理事件的死循環,這纔是它啓動後主隊列一直運行在一個主線程上的緣由。最後爲了測試啓動runloop
對串行隊列的影響,單獨啓動子隊列和一塊兒啓動後,發現另外一個現象:
runloop
一旦啓動,就只會被該線程執行任務runloop
沒法綁定隊列和線程的執行關係因爲在源碼中async
調用對於主隊列和子隊列的表現不一樣,後者會直接啓用一個線程來執行子隊列的任務,這就是致使了runloop
在主隊列和子隊列上差別化的緣由,也能說明蘋果並無大肆修改libdispatch
的源碼。
若是你看過runloop
相關的博客或者文檔,那麼應該會它是一個不斷處理消息、事件的死循環,但死循環是會消耗大量的cpu
資源的(自旋鎖就是死循環空轉)。runloop
爲了提升線程的使用效率以及減小沒必要要的損耗,在沒有事件處理的時候,假如此時存在timer、port、source
任一一種,那麼進入休眠狀態;假如不存在三者其中之一,那麼runloop
將會退出
所以爲了探討runloop
的喚醒,咱們能夠經過添加一個空端口來維持runloop
的運轉:
CFRunLoopRef runloop = NULL;
NSThread *thread = [[NSThread alloc] initWithBlock: ^{
runloop = [NSRunLoop currentRunLoop].getCFRunLoop;
[[NSRunLoop currentRunLoop] addPort: [NSMachPort new] forMode: NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}];
複製代碼
這裏主要討論的是倉鼠大佬的第五題,原問題能夠直接到最下面翻連接。主要要說明的是問題中提到的兩個api
,用於添加任務到這個runloop
中:
CFRunLoopPerformBlock(runloop, NSRunLoopCommonModes, ^{
NSLog(@"runloop perform block 1");
});
[NSObject performSelector: @selector(log) onThread: thread withObject: obj waitUntilDone: NO];
CFRunLoopPerformBlock(runloop, NSRunLoopCommonModes, ^{
NSLog(@"runloop perform block 2");
});
複製代碼
上面的代碼若是去掉了第二個perform
調用,那麼第一個調用不會輸出,反之就會都輸出。從名字上看,兩個調用都是往所在的線程裏面添加執行任務,區別在於後者的調用實際上並非直接插入任務block
,而是將任務包裝成一個timer
事件來添加,這個事件會喚醒runloop
。固然,前提是runloop
處在休眠中。
CFRunLoopPerformBlock
提供了往runloop
中添加任務的功能,但又不會喚醒runloop
,在事件不多的狀況下,這個api
能有效的減小線程狀態切換的開銷
過了一個漫長的春節假期以後,感受急需一個節假日來休息,惋惜這只是奢望。因爲節後綜合徵,在這周從新返工的狀態感受通常,也偶爾會提不起神來,但願本身儘快恢復過來。另外隨着不斷的積累,一些自覺得熟悉的奇怪問題又總能帶來新的認知和收穫,我想這就是學習最大的快樂了
因爲Swift
語法上和OC
始終存在差別,第二段代碼並不能很好的還原,若是對此感興趣的朋友能夠關注下方倉鼠大佬
的博客連接,大佬放話後續會放出源碼。另外若是不想閱讀libdispatch
源碼又想對這部分的邏輯有所瞭解的朋友能夠看下面的連接文章