關於GCD的基礎知識,以前寫過一篇博客,詳見GCD基礎知識。雖然以前已經梳理過了,但對不少知識點的理解仍然不夠透徹…寫這篇博客的緣由是在閱讀AFNetworking代碼時遇到一些奇怪的代碼。html
以下:安全
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler { __block NSURLSessionDataTask *dataTask = nil; dispatch_sync(url_session_manager_creation_queue(), ^{ dataTask = [self.session dataTaskWithRequest:request]; }); [self addDelegateForDataTask:dataTask completionHandler:completionHandler]; return dataTask; }
這段代碼是在AFURLSessionManager中定義的,用於建立Data Task(NSURLSessionDataTask實例),這段代碼令我比較奇怪的地方在於:使用dispatch_sync同步派發的方式提交任務。這短短几行(6~13行)代碼的邏輯是:網絡
爲何這麼處理呢?爲何以dispatch_sync的方式處理「建立Data Task」呢?基於以前的知識,個人猜想以下:session
不管如何,我得對dispatch_sync有進一步的理解了。多線程
關於dispatch_sync代碼的執行邏輯,恐怕不少人都已經知道了。以以下代碼爲例:async
NSLog(@"step1"); dispatch_sync(aDispatchQueue, ^{ NSLog(@"step2"); //block具體代碼 }); NSLog(@"step3");
簡而言之,dispatch_sync()中的block會被同步派發,其上下文會被阻塞,直到dispatch_block派發的block被執行完成,這段代碼的執行結果必定是:ide
step1
step2
step3
但問題是,dispatch_sync()的block的執行線程和dispatch_sync上下文所對應的線程是一個線程嗎?函數
關於這個問題曾在GCD基礎知識中闡述過,也一直覺得:學習
dispatch_sync所派發的block的執行線程和dispatch_sync上下文線程是同一個線程(不管上述代碼中的aDispatchQueue是serial dispatch queue仍是concurrent dispatch queue)。優化
但在閱讀網絡資源的時候發現有這麼一種說法:
編譯器會根據實際狀況優化代碼,因此有時候你會發現block其實還在當前線程上執行,並沒用產生新線程。
P.S:這個說法來自於iOS多線程的初步研究(八)— dispatch隊列。
顯然,網友的這個說法的言外之意是「dispatch_sync中的block的執行線程可能就是dispatch_sync上下文所在的線程,也可能不是」。
不由對本身以前的理解產生了懷疑。孰是孰非?查了不少的資料,但都沒能獲得一個確切的答案。後來詢問了一些不是相對權威的前輩,他們都支持「dispatch_sync所派發的block的執行線程和dispatch_sync上下文線程是同一個線程」這種說法。
我也經過示例代碼作了一些驗證:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"current thread 1: %@", [NSThread currentThread]); dispatch_sync(dispatch_queue_create("I.am.a.serial.dispatch.queue", DISPATCH_QUEUE_SERIAL), ^{ NSLog(@"current thread 2: %@", [NSThread currentThread]); }); dispatch_sync(dispatch_queue_create("I.am.a.concurrent.dispatch.queue", DISPATCH_QUEUE_CONCURRENT), ^{ NSLog(@"current thread 3: %@", [NSThread currentThread]); }); } /* 執行結果 current thread 1: <NSThread: 0x7fd96b42ca50>{number = 1, name = main} current thread 2: <NSThread: 0x7fd96b42ca50>{number = 1, name = main} current thread 3: <NSThread: 0x7fd96b42ca50>{number = 1, name = main} */
示例代碼的執行結果顯然說明dispatch_sync中的block的執行線程老是和dispatch_sync所在的上下文是同一個線程。但我依然懷疑「是否是dispatch_sync中處理的事情太簡單了?弄複雜一點看看」,因而我作了以下修改:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"current thread 1: %@", [NSThread currentThread]); dispatch_sync(dispatch_queue_create("I.am.a.serial.dispatch.queue", DISPATCH_QUEUE_SERIAL), ^{ dispatch_queue_t aConcurrentDispatchQueue = dispatch_queue_create("he", DISPATCH_QUEUE_CONCURRENT); __block long sum = 0; for (int i = 0; i < 10000; ++i) { dispatch_async(aConcurrentDispatchQueue, ^{ sum += i; }); } NSLog(@"current thread 2: %@", [NSThread currentThread]); }); dispatch_sync(dispatch_queue_create("I.am.a.concurrent.dispatch.queue", DISPATCH_QUEUE_CONCURRENT), ^{ dispatch_queue_t aSerialDispatchQueue = dispatch_queue_create("ha", DISPATCH_QUEUE_SERIAL); __block long sum = 0; for (int i = 0; i < 10000; ++i) { dispatch_sync(aSerialDispatchQueue, ^{ sum += i; }); } NSLog(@"current thread 3: %@", [NSThread currentThread]); }); }
不管如何增長了dispatch_sync中的block的複雜程度。獲得的結果並沒有區別。
如此這般,我想我能夠認定以前的說法:
「dispatch_sync派發的block的執行線程老是和dispatch_sync所在的上下文是同一個線程」。
相信之後能發現更加權威的文檔資料來佐證這個觀點。
回到本文開頭部分的代碼:
__block NSURLSessionDataTask *dataTask = nil; dispatch_sync(url_session_manager_creation_queue(), ^{ dataTask = [self.session dataTaskWithRequest:request]; }); [self addDelegateForDataTask:dataTask completionHandler:completionHandler]; return dataTask;
上述url_session_manager_creation_queue()函數返回的實際上是一個serial dispatch queue,這種組合即所謂的「串行同步隊列」。
通過上文的分析,這段代碼中使用「串行同步隊列」建立NSURLSessionDataTask實例顯然不是爲了提升效率,那麼它的意義是什麼呢?
好吧,既然搞不懂,就查資料吧!
翻開《Effective Objective-C 2.0》,看到item41 — 《多用派發隊列,少用同步鎖》,我想,這段代碼中使用「串行同步隊列」的惟一解釋是:爲了保證[self.session dataTaskWithRequest:request]
的線程安全。
至於爲何要保證[self.session dataTaskWithRequest:request]
這個操做的線程安全,考慮到dataTaskWithRequest:這個方法是在iOS中定義的(只提供了接口,看不到源碼),此處就不做詳細分析了,我猜想這個方法不是「線程安全」方法吧。
學習GCD固然要讀Apple的官方文檔《Concurrency Programming Guide》,其中包括關於使用dispatch_sync的提示:
Important: You should never call the dispatch_sync or dispatch_sync_f function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
簡單來講,在dispatch_sync嵌套使用時要注意:不能在一個嵌套中使用同一個serial dispatch queue,由於會發生死鎖;
假設有以下這麼一段代碼要執行:
- (void)test { dispatch_queue_t aSerialDispatchQueue = dispatch_queue_create("I.am.an.iOS.developer", DISPATCH_QUEUE_SERIAL); dispatch_sync(aSerialDispatchQueue, ^{ // block 1 NSLog(@"Good Night, Benjamin."); dispatch_sync(aSerialDispatchQueue, ^{ // block 2 NSLog(@"Good Night, Daisy."); }); }); }
本身試着執行如下就會發現:Good Night, Daisy.
這一句永遠都沒法被打印出來,緣由很簡單,程序產生了死鎖。爲何會產生死鎖呢?
能夠想象aSerialDispatchQueue在底層實現中有一把「鎖」,這把鎖確保serial dispatch queue中只有一個block被執行,當執行到block 1代碼時,這把鎖爲block 1所持有,當block 1執行完了,會釋放之;然而block 1同步派發了一個任務block 2,同步派發意味着block 1會被阻塞,直到block 2被執行完成;可是這裏產生了矛盾,block 2順利執行的前提是aSerialDispatchQueue的這把「鎖」被block 1釋放,可是block 1釋放這把「鎖」的前提是block 1執行完成,而block 1執行完的前提是block 2執行完成;因此形成的局面是「block 2等待block 1執行完成置放‘鎖’」,同時「block 1等待block 2執行完成」,這就是典型的deadlock。
這一段代碼還好,比較容易避免,可是若是對GCD理解不深,更多的時候容會寫出以下代碼:
- (void)viewDidLoad { [super viewDidLoad]; // 巴拉巴拉,作了不少事情 NSLog(@"Good Night, Benjamin."); dispatch_sync(dispatch_get_main_queue(), ^{ // refresh UI NSLog(@"Good Night, Daisy."); }); }
這段代碼的問題其實和上一段代碼相似,只不過這裏的serial dispatch queue剛好是main queue。
上述的deadlock問題主要針對「同步串行隊列」,對於「同步並行隊列」,根據個人理解應該不存在這個deadlock問題,可是《Concurrency Programming Guide》明確說了:
…This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
P.S:目前還不理解《Concurrency Programming Guide》中的這個說辭。
P.S:根據個人理解,「串行隊列」在底層實現中應該有一把「鎖」用來保證「串行隊列」中的block有且僅有一個block被執行;可是「並行隊列」的實現機制是啥呢?但願之後能回答這個問題。也許只有回答了這個問題,才能理解「… but should also be avoided for concurrent queues」。
參考資料: