深入淺出 GCD 線程使用

串行與并行

同步和異步針對(duì)的是線程隊(duì)列,所謂的線程隊(duì)列可以理解為一組線程的數(shù)組。

串行隊(duì)列:
隊(duì)列中是事件有序執(zhí)行,遵循 FIFO(first in first out)的原則,先進(jìn)入隊(duì)列的事件先執(zhí)行。

串行隊(duì)列創(chuàng)建:

dispatch_queue_t queue = dispatch_queue_create("com.queue.serial", DISPATCH_QUEUE_SERIAL);

dispatch_get_main_queue() // 主隊(duì)列,也是串行隊(duì)列

并行隊(duì)列
并行隊(duì)列中的事件在邏輯上是一起執(zhí)行的,但是這是要根據(jù)機(jī)器 CPU 的情況而定,在 C++ 線程庫中,std::thread::hardware_concurrency() 能獲取到當(dāng)前機(jī)器最大能并發(fā)的線程數(shù)量,iPhone6P 中為 2,也就是說最大同時(shí)能處理兩個(gè)并發(fā)線程任務(wù),其他后面添加的任務(wù)都得等待兩個(gè)任務(wù)中的其中一個(gè)執(zhí)行完了,才可以執(zhí)行。

dispatch_queue_t queue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 全局并發(fā)隊(duì)列

同步和異步

同步和異步針對(duì)的是線程,那么什么是同步線程,什么是異步線程。

同步線程:
阻塞當(dāng)前線程,要等待同步線程內(nèi)的任務(wù)執(zhí)行完了并且返回以后,才可以繼續(xù)執(zhí)行被阻塞線程的事件。

同步線程創(chuàng)建:

dispatch_sync(queue, block);

異步線程:
不阻塞當(dāng)前線程,等當(dāng)前線程完成時(shí)間片(完成當(dāng)前事件)切換后再執(zhí)行異步線程。

異步線程創(chuàng)建:

dispatch_async(queue, block);

線程問題

主線程中的死鎖
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^(){
    NSLog(@"2");
});
NSLog(@"3");

輸出:1

如果上面代碼是在主線程當(dāng)中執(zhí)行的,那么就會(huì)造成我們的死鎖問題,注意是主線程當(dāng)中,后面我們還有一個(gè)測(cè)試說明。
假定上面代碼為主線程中執(zhí)行的代碼,如果不造成死鎖的情況是輸出應(yīng)該是 1,2,3,但現(xiàn)在事件只執(zhí)行了 1,那么死鎖就很明顯了,我們現(xiàn)在對(duì)它進(jìn)行分析。

dispatch_sync 同步線程,將當(dāng)前線程阻塞,先執(zhí)行block(@"2") 然后解放線程
dispatch_get_main_queue 主線程隊(duì)列,也可以叫做串行隊(duì)列,將 dispatch_sync 同步線程放到隊(duì)列后,先執(zhí)行 ( @"3") 再執(zhí)行同步線程,遵循 FIFO 的原則。
當(dāng)時(shí)因?yàn)?dispatch_sync 是在主線程創(chuàng)建的,所以主線程被阻塞,主線程的事件(@"3") 要等待 dispatch_sync 的 block 執(zhí)行完后才能執(zhí)行
所以事件(@"3")無法執(zhí)行,事件(@"2")更無法執(zhí)行,相互等待造成死鎖。

dispatch_sync(dispatch_get_main_queue(), block)是否一定會(huì)造成死鎖呢?上面問題如果并不是放在主線程中有會(huì)怎么樣?

NSLog(@"1");
dispatch_queue_t queue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue), ^(){
    NSLog(@"2");
    dispatch_sync(dispatch_get_main_queue(), ^(){
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

輸出: 1,5,2,3,4

輸出中,可以看得出所有事件全部都執(zhí)行完成,沒有造成死鎖,但是明明使用了 dispatch_sync(dispatch_get_main_queue(), block);這個(gè)經(jīng)常被說成會(huì)造成死鎖的方法,但是為什么這里沒有造成死鎖呢,我們來分析一下。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,block) 中, dispatch_async 異步線程,將其放在了 dispatch_get_global_queue 全局隊(duì)列,也可以叫并行隊(duì)列中,主線程不用等待異步 dispatch_async 內(nèi)的事件(block)執(zhí)行完成,所以直接執(zhí)行了事件(@“1”)和事件(@"5")。當(dāng)線程時(shí)間片切換出來,異步線程內(nèi)的事件(block)便開始執(zhí)行了,所以事件(@"2") 便執(zhí)行了。
當(dāng)運(yùn)行到 dispatch_sync(dispatch_get_main_queue(),block) 中,dispatch_sync 阻塞當(dāng)前線程,細(xì)想一下,當(dāng)前線程是一個(gè)異步線程并不是主線程,事件(@"4")又是在這個(gè)異步線程中的事件,所以要等待 dispatch_sync 同步線程內(nèi)的事件執(zhí)行完了,才可以執(zhí)行。同步線程放在 dispatch_get_main_queue 主線程隊(duì)列中,主線程隊(duì)列同時(shí)也是一個(gè)串行隊(duì)列,所以事件(@"3") 一定會(huì)在事件@("1")和事件(@"5")之后,當(dāng)執(zhí)行完事件(@"3")便可以執(zhí)行事件(@"4")了。

上面例子說明一件事,dispatch_async 同步線程會(huì)阻塞當(dāng)前線程直至同步線程內(nèi)的事件(block)執(zhí)行完,至于是否會(huì)發(fā)生死鎖,就得看同步線程所阻塞的線程是否存在它的線程隊(duì)列(queue)中。

current thread

dispatch_sync(queue), block)

第一個(gè)例子中,current thread 為主線程,queue 主線程隊(duì)列,主線程屬于主線程隊(duì)列,所以造成死鎖。

第二個(gè)例子中,current thread 為我們所開啟的異步線程 dispatch_async,并且放在我們自己所創(chuàng)建的 dispatch_queue_t queue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT); 異步線程隊(duì)列中,queue 為主線程隊(duì)列,異步線程 dispatch_async 并不屬于主線程隊(duì)列中,所以并沒有造成死鎖。

異步串行隊(duì)列和同步串行隊(duì)列

首先我們做一個(gè)比較,在串行隊(duì)列中開啟一個(gè)異步線程,然后再異步線程的事件中再開啟一個(gè)同步線程。(默認(rèn)下面例子都是在主線程中運(yùn)行)

dispatch_queue_t queue = dispatch_queue_create("com.queue.CONCURRENT", DISPATCH_QUEUE_CONCURRENT);

NSLog(@"1");
dispatch_async(queue, ^() {
    NSLog(@"2");
    dispatch_sync(queue, ^(){
      NSLog(@"3");
    });
    NSLog(@"4");
});

NSLog(@"5");

輸出:1,5,2,3,4

然后將 queue 換成一個(gè)串行隊(duì)列,看看效果如何

dispatch_queue_t queue2 = dispatch_queue_create("com.queue.SERIAL", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");
dispatch_async(queue2, ^() {
    NSLog(@"2");
    dispatch_sync(queue2, ^(){
      NSLog(@"3");
    });
    NSLog(@"4");
});

NSLog(@"5");

輸出:1,5,2

第一個(gè)例子使用 DISPATCH_QUEUE_CONCURRENT 并發(fā)隊(duì)列,輸出正常,而第二個(gè)例子中使用了 DISPATCH_QUEUE_SERIAL 串行隊(duì)列,發(fā)生了死鎖,后面的事件 (@"3") 和事件 (@"4")便無法執(zhí)行。

我們首先分析一下第一個(gè)例子,為什么并沒有發(fā)生死鎖,首先我們往并發(fā)隊(duì)列 queue 中添加了dispatch_async 異步線程 ,主線程并不等待異步線程的執(zhí)行,所以事件 (@"1") 后便馬上執(zhí)行事件 (@"5"),當(dāng)內(nèi)核線程空閑,加載并發(fā)隊(duì)列 queue 中的 dispatch_async 異步線程 并執(zhí)行線程中的事件(block) 的,事件 (@"2") 馬上就會(huì)被執(zhí)行。
當(dāng)遇到了 dispatch_sync 同步線程的時(shí)候,當(dāng)前線程,也就是 dispatch_async 這個(gè)異步線程會(huì)進(jìn)入阻塞,等待 dispatch_sync 同步線程內(nèi)的事件(block) 執(zhí)行完,才可以往下執(zhí)行事件(@"4"),我們并將dispatch_sync 同步線程放進(jìn)了 queue 并發(fā)隊(duì)列當(dāng)中去,并發(fā)隊(duì)列的特點(diǎn)就是邏輯上是一起執(zhí)行的,所以 dispatch_sync 同步線程加入 queue 后就馬上被執(zhí)行了,當(dāng)事件(@"3")執(zhí)行完后并且返回,阻塞放開,事件(@"4")并馬上被執(zhí)行。全過程并沒有發(fā)生死鎖

我們?cè)賮砜纯吹诙€(gè)例子,首先我們往串行隊(duì)列 queue 中添加了dispatch_async 異步線程 ,其后過程跟第一個(gè)例子一樣,直到遇到了 dispatch_sync(queue2, block) ,dispatch_sync` 同步線程 阻塞了 dispatch_async 異步線程,并將同步線程放進(jìn)了 queue2 串行隊(duì)列中,串行隊(duì)列的特別是遵循 FIFO 特點(diǎn),要必先執(zhí)行完 dispatch_async 異步線程的事件(block),才能執(zhí)行同步線程 dispatch_sync 的事件 (block),所以造成了死鎖。

AFNetWorking 怎么使用同步線程
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                  withReceiptID:(nonnull NSUUID *)receiptID
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
                                                        
    dispatch_sync(self.synchronizationQueue, ^{
            NSString *URLIdentifier = request.URL.absoluteString;
            if (URLIdentifier == nil) {
                if (failure) {
                    NSError *error;
                    dispatch_async(dispatch_get_main_queue(), ^{
                        failure(request, nil, error);
                    });
                }
                return;
            }
        
            ...
    });
}

上面一段代碼才子 AFNetWorking 中的 AFImageDownloader.m 文件當(dāng)中,作者創(chuàng)建了 synchronizationQueue 串行隊(duì)列專門用作阻塞當(dāng)前線程,限制性同步隊(duì)列中的事件,判斷 url 是否為空,但是為什么要這樣做呢?

原因:
因?yàn)閷?duì)象方法 downloadImageForURLRequest:withReceiptID:success:failure 是同一個(gè)對(duì)象在多個(gè)異步線程的并發(fā)隊(duì)列當(dāng)中執(zhí)行的,因?yàn)椴l(fā)在邏輯上會(huì)同時(shí)觸發(fā)異步線程,那么傳進(jìn)來的參數(shù)(request,receiptID,success,failure)會(huì)由于資源競(jìng)爭(zhēng)(condition race) 的情況下會(huì)被覆蓋,所以我們需要進(jìn)行阻塞這個(gè)線程,先執(zhí)行完一個(gè)請(qǐng)求后再執(zhí)行另外一個(gè)請(qǐng)求

但是會(huì)有人問:為什么么不用 @synchronized (<#lock#>) {} ?
因?yàn)槲覀兪紫炔淮_定調(diào)用對(duì)象方法 downloadImageForURLRequest:withReceiptID:success:failure 是否必定在異步線程中被調(diào)用,莫名的加鎖會(huì)消耗資源,當(dāng)我們使用了 dispatch_sync(self.synchronizationQueue,block) 后,如果主線程當(dāng)中被調(diào)用,也只會(huì)忽視這個(gè)方法,直接調(diào)用 block,因?yàn)樽枞骶€程,往并不是主線程隊(duì)列的線程隊(duì)列中添加事件,是沒有意義的。

使用 dispatch_sync(self.synchronizationQueue,block) 需要注意什么問題?
其實(shí)上面這么寫,是有問題的,當(dāng)方法 downloadImageForURLRequest:withReceiptID:success:failure 的調(diào)用上層,也是dispatch_sync(self.synchronizationQueue,block) 的情況下,就會(huì)造成死鎖,就像下面一樣

dispatch_sync(self.synchronizationQueue, ^(){
    NSLog(@"2");
    dispatch_sync(self.synchronizationQueue, ^(){
      NSLog(@"3");
    });
    NSLog(@"4");
});

dispatch_async(self.synchronizationQueue, ^(){
    NSLog(@"2");
    dispatch_sync(self.synchronizationQueue, ^(){
      NSLog(@"3");
    });
    NSLog(@"4");
});

至于怎么分析,為什么會(huì)發(fā)生死鎖,各位看官,這就留給你們的作業(yè),看了這么多,相信大家也會(huì)明白,特別是第二個(gè)例子,我們剛講過,希望大家能在這篇博客中學(xué)到東西。


寫在最后:

為什么要寫這篇文章呢?主要今天在某公司面試的時(shí)候,被問到了關(guān)于 GCD 的線程問題,在我說出來答案后,面試官依然堅(jiān)持已見,認(rèn)為我是錯(cuò)的,讓該面試官指出哪里錯(cuò)誤的時(shí)候,該面試官又在故弄玄虛,并讓我錯(cuò)失了這個(gè)寶貴的機(jī)會(huì),寫這篇博客的目的在于,不管這個(gè)面試官是否會(huì)游覽博客,也讓更多的面試官可以好好更新自己的知識(shí)儲(chǔ)備庫,不要坐井觀天。其實(shí)在我看來,面試是一個(gè)雙向交流的過程,我并不在意是否能你們公司工作,畢竟我也不想與一群無法交流的人共事,一個(gè)開心愉快并且能夠助我成長(zhǎng)的工作環(huán)境才是我真正需要的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容