因為本文只做分享用,非學術性文章,所以某些理論并不是非常嚴謹,望大家見諒。寫下這篇文章有以下的目:
1. 鞏固自己的知識,只有把自己知道的東西系統(tǒng)地組織出來,才知道自己到底知不知道。
2. 分享心得,希望剛入門開發(fā)的朋友,能夠知其然且知其所以然,而不是僅僅死記硬背哪些情況會造成死鎖。
3. 希望看到我博客的朋友,能夠為我指出我理解中的不足與錯誤之處,共同進步。
- 我寫這篇文章時,假設你已具備:
- GCD的基礎知識,能夠使用
一、搞清線程(Thread)和隊列(Queue)的區(qū)別
網(wǎng)上一些講解關于GCD死鎖的文章,有一些非常明顯的錯誤,比如:認為死鎖的原因是線程阻塞造成的,這是非常大的誤解,GCD死鎖的原因是隊列阻塞,而不是線程阻塞!

在開發(fā)中,我們會把block(也就是swift中的closure),也就是我們想做的任務,交給GCD函數(shù)。GCD函數(shù)會把任務放進我們指定的隊列(Queue),當然GCD函數(shù)內部不止是把任務放進隊列,還包括一些其他不為我們所知的操作。隊列遵循嚴格的先進先出原則,同一個Queue中,最早入列的block,會最早被分配給線程執(zhí)行。系統(tǒng)(“系統(tǒng)”指所有被蘋果黑盒封裝,未公開源碼,我們不能得知的操作,下同)會依據(jù)順序從隊列中取出block,并且交由線程執(zhí)行。GCD隊列只是組織待執(zhí)行任務的一個數(shù)據(jù)結構封裝,而線程,才是執(zhí)行任務的人。
二、回顧程序執(zhí)行順序
要往下面講,不得不回顧一個再基礎不過的知識點,我想,這是每一個程序員,入門就知道的超級簡單的知識。雖然它非?;A,但是,這正是造成我們GCD死鎖的重要因素。很多困難的問題,它們背后隱藏的東西往往非常簡單,因為事物永遠不會脫離本質。
讓我們來看看下面的這個C程序:
#include <stdio.h>
void printFiveNumbers(){
printf("開始執(zhí)行printFiveNumbers函數(shù)了\n");
for (int i = 0; i < 5; i++) {
printf("printFiveNumbers - %d\n",i);
}
printf("執(zhí)行完printFiveNumbers函數(shù)了\n");
}
//main函數(shù)是程序的入口
int main(){
printf("main函數(shù)開始執(zhí)行了\n");
printFiveNumbers();
printf("main函數(shù)執(zhí)行完了\n");
return 0;
}

大家都知道,運行的結果是怎么樣了,程序的入口是main函數(shù),于是Run這個程序后,馬上就會進入main函數(shù)執(zhí)行,執(zhí)行了第一句打印后,會跳入printFiveNumbers這個函數(shù)執(zhí)行,直到printFiveNumbers執(zhí)行完,才會返回到main函數(shù)繼續(xù)執(zhí)行下一句。重點是:
外層方法會等待內層方法返回后,再執(zhí)行下一句指令。就好像把printFiveNumbers函數(shù)的所有語句,都復制粘貼到了main方法里一樣。
三、GCD死鎖的本質
讓我們看看下面這個程序:
override func viewDidLoad() {
super.viewDidLoad()
print("Start \(NSThread.currentThread())")
//GCD同步函數(shù)
dispatch_sync(dispatch_get_main_queue(), {
for i in 0...100{
print("\(i) \(NSThread.currentThread())")
}
})
print("End \(NSThread.currentThread())")
}

這個程序就是典型的死鎖,可以看到,只打印了“Start”一行,就再也沒有響應了,已經(jīng)造成了GCD死鎖。為什么會這樣呢?讓我們來解讀一下這段程序的運行順序:首先會打印“Start”,然后將主隊列和一個block傳入GCD同步函數(shù)dispatch_sync中,等待sync函數(shù)執(zhí)行,直到它返回,才會執(zhí)行打印“End”的語句??墒?,竟然沒有反應了?block中的101個數(shù)字沒有被打印出來任何一個,viewDidLoad()中的End也沒有被打印出來。也就是說,block沒有得到執(zhí)行的機會,viewDidLoad也沒有繼續(xù)執(zhí)行下去。為什么block不執(zhí)行呢?因為viewDidLoad也是執(zhí)行在主隊列的,它是正在被執(zhí)行的任務,也就是說,viewDidLoad()是主隊列的隊頭。主隊列是串行隊列,任務不能并發(fā)執(zhí)行,同時只能有一個任務在執(zhí)行,也就是隊頭的任務才能被出列執(zhí)行。我們現(xiàn)在被執(zhí)行的任務是viewDidLoad(),然后我們又將block入列到
同一個隊列,它比viewDidLoad()后入列,遵循先進先出的原理,它必須等到viewDidLoad()執(zhí)行完,才能被執(zhí)行。但是,dispatch_sync函數(shù)的特性是,等待block被執(zhí)行完畢,才會返回,因此,只要block一天不被執(zhí)行,它就一天不返回。我們知道,內部方法不返回,外部方法是不會執(zhí)行下一行命令的。不等到sync函數(shù)返回,viewDidLoad打死也不會執(zhí)行print End的語句,因此,viewDidLoad()一直沒有執(zhí)行完畢。block在等待著viewDidLoad()執(zhí)行完畢,它才能上,sync函數(shù)在等待著block執(zhí)行完畢,它才能返回,viewDidLoad()在等待著sync函數(shù)返回,它才能執(zhí)行完畢。這樣的三方循環(huán)等待關系,就造成了死鎖。也許文字描述比較抽象,我們再來配一幅圖:

可以這么理解:每一個隊列,有自己的執(zhí)行室,串行隊列的執(zhí)行室,只能容納一個任務,并發(fā)隊列的執(zhí)行室,可以同時容納若干個任務。隊頭的任務,只要執(zhí)行室有空位,就會被放入執(zhí)行室執(zhí)行。viewDidLoad任務在執(zhí)行中,我們的主隊列又是串行隊列,執(zhí)行室只能容納一個任務,那么隊頭的block就需要等待viewDidLoad執(zhí)行完畢才能進入執(zhí)行室,那么就造成了,viewDidLoad永遠不會執(zhí)行完畢,block永遠不能執(zhí)行。

sync函數(shù)永遠不能返回,最終,就是GCD死鎖。
- 那么我們可以總結出GCD被阻塞(blocking)的原因有以下兩點:
- GCD函數(shù)未返回,會阻塞正在執(zhí)行的任務
- 隊列的執(zhí)行室容量太小,在執(zhí)行室有空位之前,會阻塞同一個隊列中在等待的任務
注意:阻塞(blocking)和死鎖(deadlock)是不同的意思,阻塞表示需要等待A事件完成后才能完成B事件,稱作A會阻塞B,通俗來講就是強制等待的意思。而死鎖表示由于某些互相阻塞,也就是互相的強制等待,形成了閉環(huán),導致大家永遠互相阻塞下去了,Always and Forever,也就是死鎖。
以上兩點阻塞情景,同時只出現(xiàn)一個,并不會出現(xiàn)死鎖,但是如果兩個同時出現(xiàn),就會出現(xiàn)阻塞閉環(huán),造成死鎖。因此,造成GCD死鎖的原因就是同時具備這兩個因素,只要大家理解了這點,就再也不用死記硬背哪些情況會造成GCD死鎖了。
四、解決GCD死鎖
我們已經(jīng)有結論,造成GCD死鎖,是由于同時具備以下兩點因素:
1. GCD函數(shù)未返回,會阻塞正在執(zhí)行的任務
2. 隊列的執(zhí)行室容量太小,在執(zhí)行室有空位之前,會阻塞同一個隊列中在等待的任務
死鎖是由于阻塞閉環(huán)造成的,那么我們只用消除其中一個因素,就能打破這個閉環(huán),避免死鎖。
#######方法1:解決GCD函數(shù)未返回造成的阻塞
先提出兩個知識點:
- dispatch_sync是同步函數(shù),不具備開啟新線程的能力,交給它的block,只會在當前線程執(zhí)行,不論你傳入的是串行隊列還是并發(fā)隊列,并且,
它一定會等待block被執(zhí)行完畢才返回。
- dispatch_sync是同步函數(shù),不具備開啟新線程的能力,交給它的block,只會在當前線程執(zhí)行,不論你傳入的是串行隊列還是并發(fā)隊列,并且,
- dispatch_async是異步函數(shù),具備開啟新線程的能力,但是不一定會開啟新線程,交給它的block,可能在任何線程執(zhí)行,開發(fā)者無法控制,是GCD底層在控制。
它會立即返回,不會等待block被執(zhí)行。
注意:以上兩個知識點,有例外,那就是當你傳入的是主隊列,那兩個函數(shù)都一定會安排block在主線程執(zhí)行。記住,主隊列是最特殊的隊列
只要看懂了以上兩個知識點,大家就知道,sync函數(shù)未返回會造成阻塞,只要換成aysnc函數(shù),就會立即返回,而不會等待block執(zhí)行,那么GCD函數(shù)未返回這個阻塞因素就會被解決掉。不用大家也不要盲目的換函數(shù),畢竟兩個函數(shù)是有不同之處的,要考慮實際期望。
- dispatch_async是異步函數(shù),具備開啟新線程的能力,但是不一定會開啟新線程,交給它的block,可能在任何線程執(zhí)行,開發(fā)者無法控制,是GCD底層在控制。
#######方法2:解決隊列(Queue)阻塞
解決隊列阻塞,有兩種方法:
- 為隊列的執(zhí)行室擴容,讓它可以并發(fā)執(zhí)行多個任務,那么就不會因為A任務,造成B任務被阻塞了。
- 把A和B任務放在兩個不同的隊列中,A就再也沒有機會阻塞B了。因為每個隊列都有自己的執(zhí)行室。
首先來說第一個思路,如何為隊列的執(zhí)行室擴容呢?我們當然沒有辦法為執(zhí)行室擴容,但是我們可以選擇用容量大的隊列。使用并發(fā)隊列替代串行隊列。因為并發(fā)隊列的執(zhí)行室可以同時容納若干任務
再來說第二個思路,我們來看代碼:
override func viewDidLoad() {
super.viewDidLoad()
print("Start \(NSThread.currentThread())")
let serialQueue = dispatch_queue_create("這是一個串行隊列", DISPATCH_QUEUE_SERIAL)
dispatch_sync(serialQueue, {
for i in 0...100{
print("\(i) \(NSThread.currentThread())")
}
})
print("End \(NSThread.currentThread())")
}

我們自己新建了一個串行隊列,將block放入自己的串行隊列,不再和viewDidLoad()處于一個隊列,解決了隊列阻塞,因此避免了死鎖問題。
網(wǎng)上有一些帖子說“在主線程使用sync函數(shù)就會造成死鎖”或者“在主線程使用sync函數(shù),同時傳入串行隊列就會死鎖”,都是非常錯誤的觀念,希望大家能夠真正理解GCD死鎖的原理,而不是死記硬背。