開始之前
首先要解決一個大家對多線程的理解上可能存在的誤區(qū):新開一個線程,能提高速度,避免阻塞主線程。
這句話看著好像是對著呢,但是仔細(xì)想想這句話是不那么準(zhǔn)確的。
舉個例子:一個主任務(wù)需要十個子任務(wù)按順序執(zhí)行來完成。現(xiàn)在有兩種方式完成這個任務(wù):
1.建十個線程,把每個子任務(wù)放在對應(yīng)的線程中執(zhí)行。執(zhí)行完一個線程中的任務(wù)就切換到另一個線程。
2.把十個任務(wù)放在一個線程里,按順序執(zhí)行。
操作系統(tǒng)的基礎(chǔ)知識告訴我們,線程,是執(zhí)行程序最基本的單元,它有自己棧和寄存器。說得再具體一些,線程就是“一個CPU執(zhí)行的一條無分叉的命令列”。
對于第一種方法,在十個線程之間來回切換,就意味著有十組棧和寄存器中的值需要不斷地被備份、替換。 而對于對于第二種方法,只有一組寄存器和棧存在,顯然效率完勝前者。
并發(fā)與并行的區(qū)別
并發(fā)指的是一種現(xiàn)象,一種經(jīng)常出現(xiàn),無可避免的現(xiàn)象。它描述的是“多個任務(wù)同時發(fā)生,需要被處理”這一現(xiàn)象。它的側(cè)重點在于“發(fā)生”。
比如有很多人排隊等待檢票,這一現(xiàn)象就可以理解為并發(fā)。
并行指的是一種技術(shù),一個同時處理多個任務(wù)的技術(shù)。它描述了一種能夠同時處理多個任務(wù)的能力,側(cè)重點在于“運行”。
比如景點開放了多個檢票窗口,同一時間內(nèi)能服務(wù)多個游客。這種情況可以理解為并行。
并行的反義詞就是串行,表示任務(wù)必須按順序來,一個一個執(zhí)行,前一個執(zhí)行完了才能執(zhí)行后一個。
我們經(jīng)常提到的“多線程”,正是采用了并行技術(shù),從而提高了執(zhí)行效率。因為有多個線程,所以計算機(jī)的多個CPU可以同時工作,同時處理不同線程內(nèi)的指令。
并發(fā)是一種現(xiàn)象,面對這一現(xiàn)象,我們首先創(chuàng)建多個線程,真正加快程序運行速度的,是并行技術(shù)。也就是讓多個CPU同時工作。而多線程,是為了讓多個CPU同時工作成為可能。
同步與異步
同步方法就是我們平時調(diào)用的哪些方法。比如在第一行調(diào)用a方法,那么程序運行到第二行的時候,a方法肯定是執(zhí)行完了。
所謂的異步,就是允許在執(zhí)行某一個任務(wù)時,函數(shù)立刻返回,但是真正要執(zhí)行的任務(wù)稍后完成。
比如我們在點擊保存按鈕之后,要先把數(shù)據(jù)寫到內(nèi)存,然后更新UI。同步方法就是等到數(shù)據(jù)保存完再更新UI,而異步則是立刻從保存數(shù)據(jù)的方法返回并向后執(zhí)行代碼,同時真正用來保存數(shù)據(jù)的指令將在稍后執(zhí)行。
區(qū)別和聯(lián)系
串行/并行針對的是隊列,而同步/異步,針對的則是線程。最大的區(qū)別在于,同步線程要阻塞當(dāng)前線程,必須要等待同步線程中的任務(wù)執(zhí)行完,返回以后,才能繼續(xù)執(zhí)行下一任務(wù);而異步線程則是不用等待。
假設(shè)現(xiàn)在有三個任務(wù)需要處理。假設(shè)單個CPU處理它們分別需要3、1、1秒。
并行/串行討論的是處理這三個任務(wù)的速度問題。如果三個CPU并行處理,那么一共只需要3秒。相比于串行處理,節(jié)約了兩秒。
同步/異步描述的是任務(wù)之間先后順序問題。假設(shè)需要三秒的那個是保存數(shù)據(jù)的任務(wù),而另外兩個是UI相關(guān)的任務(wù)。那么通過異步執(zhí)行第一個任務(wù),我們省去了三秒鐘的卡頓時間。
對于同步執(zhí)行的三個任務(wù)來說,系統(tǒng)傾向于在同一個線程里執(zhí)行它們。因為即使開了三個線程,也得等他們分別在各自的線程中完成。并不能減少總的處理時間,反而徒增了線程切換所耗費的時間。
對于異步執(zhí)行的三個任務(wù)來說,系統(tǒng)傾向于在三個新的線程里執(zhí)行他們。因為這樣可以最大程度的利用CPU性能,提升程序運行效率。
總結(jié)
在需要同時處理寫入寫出操作和UI操作的情況下,真正起作用的是異步,而不是多線程??梢圆挥枚嗑€程,但不能不用異步。
GCD
GCD以block(Swift中是閉包,為了方便,下面都以block表示)為基本單位,一個block中的代碼可以為一個任務(wù)。下文中提到任務(wù),可以理解為執(zhí)行某個block。
同時,GCD中有兩大最重要的概念,分別是“隊列”和“執(zhí)行方式”。
使用block的過程,概括來說就是把block放進(jìn)合適的隊列,并選擇合適的執(zhí)行方式去執(zhí)行block的過程。
三種隊列:
串行隊列(先進(jìn)入隊列的任務(wù)先出隊列,每次只執(zhí)行一個任務(wù))
并發(fā)隊列(依然是“先入先出”,不過可以形成多個任務(wù)并發(fā))
主隊列(這是一個特殊的串行隊列,而且隊列中的任務(wù)一定會在主線程中執(zhí)行)
兩種執(zhí)行方式:
同步執(zhí)行
異步執(zhí)行
關(guān)于同步/異步、串行/并行和線程的關(guān)系,下面通過一個表格來總結(jié):
| 同步 | 異步 | |
|---|---|---|
| 主隊列 | 在主線程中執(zhí)行 | 在主線程中執(zhí)行 |
| 串行隊列 | 在當(dāng)前線程中執(zhí)行 | 新建線程執(zhí)行 |
| 并發(fā)隊列 | 在當(dāng)前線程中執(zhí)行 | 新建線程執(zhí)行 |
可以看到,同步方法不一定在本線程,因為加入到主隊列的就會在主線程內(nèi)執(zhí)行;異步方法方法也不一定新開線程,也是因為主隊列的特殊情況。
在我們的實際開發(fā)過程中,我們要更多考慮的是怎么準(zhǔn)確的使用好串行/并行、同步/異步,而不是僅僅只考慮是否新開線程這個問題。
當(dāng)然,了解任務(wù)運行在那個線程中也是為了更加深入的理解整個程序的運行情況,尤其是接下來要討論的死鎖問題。
GCD的死鎖問題
在使用GCD的過程中,如果向當(dāng)前串行隊列中同步派發(fā)一個任務(wù),就會導(dǎo)致死鎖。
這句話有點繞,先舉個例子看看:
override func viewDidLoad() {
super.viewDidLoad()
let queue = DispatchQueue.main
queue.sync {
print("啊哈哈")
}
}
這段代碼就會導(dǎo)致死鎖,因為我們目前在主隊列中,又將要同步地添加一個block到主隊列中。
先分析一波
我們知道.sync表示同步的執(zhí)行任務(wù),也就是說執(zhí)行.sync后,當(dāng)前線程會阻塞。而.sync中的block如果要在當(dāng)前線程中執(zhí)行,就得等待當(dāng)前線程執(zhí)行完成。
在上面這個例子中,主線程在執(zhí)行.sync,隨后主隊列中新增一個任務(wù)block。因為主隊列是串行隊列,所以block要等.sync執(zhí)行完才能執(zhí)行,但是.sync是同步派發(fā),要等block執(zhí)行完才算是結(jié)束。在主隊列中的兩個任務(wù)互相等待,導(dǎo)致了死鎖。
解決方案
其實在通常情況下我們不必要用.sync,因為.async能夠更好的利用CPU,提升程序運行速度。只有當(dāng)我們需要保證隊列中的任務(wù)必須順序執(zhí)行時,才考慮.sync。在使用.sync的時候應(yīng)該分析當(dāng)前處于哪個隊列,以及任務(wù)會提交到哪個隊列。
DispatchGroup
在平時的開發(fā)過程中,可能會有這樣的需求:我需要在完成一些任務(wù)之后緊接著去執(zhí)行另外一個任務(wù),這里,我們就可以使用GCD任務(wù)組解決類似需求。
在單個串行隊列中,這個需求不是問題,因為只要把回調(diào)block添加到隊列末尾即可。
但是對于并行隊列,以及多個串行、并行隊列混合的情況,就需要使用DispatchGroup了。
override func viewDidLoad() {
super.viewDidLoad()
let concurrentQueue = DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
let serialQueue = DispatchQueue(label: "serialQueue")
let group = DispatchGroup()
for i in 0...3 {
concurrentQueue.async(group: group, qos: .default, flags: []) {
print("concurrentQueue\(i)")
}
}
for i in 0...3 {
serialQueue.async(group: group, qos: .default, flags: []) {
print("serialQueue\(i)")
}
}
//執(zhí)行完上面的兩個耗時操作, 回到主隊列中執(zhí)行下一步的任務(wù)
group.notify(queue: DispatchQueue.main) {
print("回到主隊列執(zhí)行一些操作")
}
}
輸出:
concurrentQueue1
serialQueue0
concurrentQueue2
concurrentQueue0
concurrentQueue3
serialQueue1
serialQueue2
serialQueue3
回到主隊列執(zhí)行一些操作
首先創(chuàng)建一個并發(fā)隊列和串行隊列,然后通過DispatchGroup()方法生成一個組。
接下來,在兩個不同的隊列里面分別加入不同的任務(wù),并放入到group中去。
最后調(diào)用group.notify方法。這個方法表示把第二個參數(shù) block 傳入第一個參數(shù)隊列中去。而且可以保證第二個參數(shù) block 執(zhí)行時,group中的所有任務(wù)已經(jīng)全部完成。
.asyncAfter方法
通過 GCD 還可以進(jìn)行簡單的定時操作,比如在 1 秒后執(zhí)行某個 block 。代碼如下:
DispatchQueue.main.asyncAfter(deadline:DispatchTime.now() + 1 ) {
print("我是在一秒后執(zhí)行的")
}
.asyncAfter方法的調(diào)用者表示要執(zhí)行的任務(wù)提交到哪個隊列,后面有兩個參數(shù)。第一個表示時間,也就是從現(xiàn)在起往后一秒鐘。第二個參數(shù)分別表示要提交的任務(wù)。
需要注意的是.asyncAfter僅表示在指定時間后提交任務(wù),而非執(zhí)行任務(wù)。如果任務(wù)提交到主隊列,它將在main runloop中執(zhí)行,對于每隔1/60秒執(zhí)行一次的RunLoop,任務(wù)最多有可能在1+1/60秒后執(zhí)行。
Operation
Operation和OperationQueue主要涉及這幾個方面:
-
Operation和OperationQueue用法介紹 -
Operation的暫停、恢復(fù)和取消 - 通過 KVO 對
Operation的狀態(tài)進(jìn)行檢測 - 多個
Operation的之間的依賴關(guān)系
從簡單意義上來說,Operation是對 GCD 中的 block 進(jìn)行的封裝,它也表示一個要被執(zhí)行的任務(wù),Operation對象有一個start()方法表示開始執(zhí)行這個任務(wù)。
不僅如此,Operation表示的任務(wù)還可以被取消。它還有三種狀態(tài)isExecuted、isFinished、isCancelled以方便我們通過 KVC 對它的狀態(tài)進(jìn)行監(jiān)聽。
想要開始執(zhí)行一個任務(wù)可以這么寫:
let operation = BlockOperation.init {
print("初始化 0 的任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 1 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 2 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 3 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 4 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 5 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 6 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 7 個添加任務(wù)\(Thread.current)")
}
operation.addExecutionBlock {
print("第 8 個添加任務(wù)\(Thread.current)")
}
operation.start()
print("結(jié)束了")
輸出內(nèi)容:
初始化 0 的任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 2 個添加任務(wù)<NSThread: 0x60000026af80>{number = 3, name = (null)}
第 1 個添加任務(wù)<NSThread: 0x608000262e80>{number = 5, name = (null)}
第 3 個添加任務(wù)<NSThread: 0x608000262d80>{number = 4, name = (null)}
第 4 個添加任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 5 個添加任務(wù)<NSThread: 0x60000026af80>{number = 3, name = (null)}
第 6 個添加任務(wù)<NSThread: 0x608000262e80>{number = 5, name = (null)}
第 8 個添加任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 7 個添加任務(wù)<NSThread: 0x608000262d80>{number = 4, name = (null)}
結(jié)束了
使用BlockOperation來創(chuàng)建是因為Operation是一個基類,不應(yīng)該直接生成Operation對象,而是應(yīng)該用它的子類。BlockOperation是蘋果預(yù)定義的子類,它可以用來封裝一個或多個 block ,后面會介紹如何自己創(chuàng)建Operation的子類。
在上面的例子里面我們創(chuàng)建了一個BlockOperation,并且設(shè)置好它的 block ,也就是將要執(zhí)行的任務(wù),同時,我們調(diào)用addExecutionBlock方法追加幾個任務(wù),這些任務(wù)會并行執(zhí)行。但是它并非是將所有的 block 都放到放到了子線程中。通過上面的打印記錄我們可以發(fā)現(xiàn),它會優(yōu)先將 block 放到主線程中執(zhí)行,若主線程已有待執(zhí)行的代碼,就開辟新的線程,但最大并發(fā)數(shù)為4(包括主線程在內(nèi),在真機(jī)上最大并發(fā)數(shù)為2,不必糾結(jié)這個,明白原理即可),如果 block 數(shù)量大于了線程的最大并發(fā)數(shù),那么剩下的 block 就會等待某個線程空閑下來之后被分配到該線程,且依然是優(yōu)先分配到主線程。
最后,調(diào)用start()方法讓Operation方法運行起來。start()是一個同步方法,也就是在調(diào)用start()方法的那個線程中直接執(zhí)行,會阻塞調(diào)用start()方法的線程。
OperationQueue
從上面我們可以知道Operation是同步執(zhí)行的。簡單的看一下 NSOperation 類的定義會發(fā)現(xiàn)它有一個只讀屬性 asynchronous,這意味著如果想要異步執(zhí)行,就需要自定Operation的子類?;蛘呤褂?code>OperationQueue。
OperationQueue類似于 GCD 中的隊列。我們知道 GCD 中的隊列有三種:主隊列、串行隊列和并行隊列。OperationQueue更簡單,只有兩種:主隊列和非主隊列。我們自己生成的OperationQueue對象都是非主隊列,主隊列可以用OperationQueue.main取得。
OperationQueue的主隊列是串行隊列,而且其中所有Operation都會在主線程中執(zhí)行。對于非主隊列來說,一旦一個Operation被放入其中,那這個Operation一定是并發(fā)執(zhí)行的。因為OperationQueue會為每一個Operation創(chuàng)建線程并調(diào)用它的start()方法。
OperationQueue有一個屬性叫maxConcurrentOperationCount,它表示最多支持多少個Operation并發(fā)執(zhí)行。如果maxConcurrentOperationCount被設(shè)為 1,就認(rèn)為這個隊列是串行隊列。需要注意的是設(shè)備最大并發(fā)數(shù)是有上限的,即使你設(shè)置maxConcurrentOperationCount為100,它也不會超過設(shè)備最大并發(fā)數(shù)上限,而這個上限的數(shù)目也是由具體運行環(huán)境決定的。
OperationQueue和GCD中的隊列有這樣的對應(yīng)關(guān)系:
| OperationQueue | GCD | |
|---|---|---|
| 主隊列 | OperationQueue.main |
DispatchQueue.main |
| 串行隊列 | 自建隊列設(shè)置maxConcurrentOperationCount為 1 |
DispatchQueue(label: "serialQueue") |
| 并發(fā)隊列 | 自建隊列設(shè)置maxConcurrentOperationCount大于 1 |
DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil |
想要使用OperationQueue實現(xiàn)異步操作可以這么寫:
let operationQueue = OperationQueue()
let operation = BlockOperation()
for i in 1...10 {
operation.addExecutionBlock {
print("第 \(i) 個添加任務(wù)\(Thread.current)")
}
}
operationQueue.addOperation(operation)
輸出內(nèi)容:
第 1 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 2 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 3 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 4 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 5 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 6 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 7 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 8 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 9 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 10 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
使用OperationQueue來執(zhí)行任務(wù)與之前的區(qū)別在于,首先創(chuàng)建一個非主隊列。然后用addOperation方法替換之前的start()方法。剛剛已經(jīng)說過,OperationQueue會為每一個Operation建立線程并調(diào)用他們的start()方法。
觀察一下運行結(jié)果,所有的Operation都沒有在主線程執(zhí)行,從而成功的實現(xiàn)了異步、并行處理。
除了上述的將Operation添加到隊列中的使用方法外,OperationQueue提供了一個更加簡單的方法,只需以下兩行代碼就能實現(xiàn)多線程調(diào)用
let operationQueue = OperationQueue()
operationQueue.addOperation {
print(Thread.current)
}
輸出內(nèi)容:
<NSThread: 0x170460d40>{number = 4, name = (null)}
你可以同時添加一個或這個多個Block來實現(xiàn)你的操作。
取消任務(wù)
如果我們有兩次網(wǎng)絡(luò)請求,第二次請求會用到第一次的數(shù)據(jù)。假設(shè)此時網(wǎng)絡(luò)情況不好,第一次請求超時了,那么第二次請求也沒有必要發(fā)送了。而且用戶也有可能人為地取消某個Operation。
當(dāng)產(chǎn)生這種需求的時候,我們就可以取消這些操作:
//取消Operation
let operation = BlockOperation.init {
print("哈哈哈哈哈哈 0")
}
operation.cancel()
//取消某個OperationQueue剩余的Operation
let operationQueue = OperationQueue()
for i in 1...10 {
operationQueue.addOperation {
print(Thread.current)
}
}
operationQueue.cancelAllOperations()
暫停和取消并不會立即暫?;蛉∠?dāng)前操作,而是不在調(diào)用新的Operation。
設(shè)置依賴
如果現(xiàn)在需要兩次網(wǎng)絡(luò)請求,第二次請求會用到第一次的數(shù)據(jù),所以我們要保證發(fā)出第二次請求的時候第一個請求已經(jīng)執(zhí)行完,但是我們同時還希望利用到OperationQueue的并發(fā)特性(因為可能不止這兩個任務(wù))。
這時候我們可以設(shè)置Operation之間的依賴關(guān)系:
//讓operation1在operation2執(zhí)行完之后執(zhí)行
let operationQueue = OperationQueue()
let operation1 = BlockOperation.init {
print("第 1 個添加任務(wù)\(Thread.current)")
}
let operation2 = BlockOperation.init {
print("第 2 個添加任務(wù)\(Thread.current)")
}
//需要注意的是Operation之間的相互依賴會導(dǎo)致死鎖,如果1依賴2,2又依賴1,就會導(dǎo)致死鎖。
operation1.addDependency(operation2)
operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)
輸入內(nèi)容:
第 2 個添加任務(wù)<NSThread: 0x17027a340>{number = 4, name = (null)}
第 1 個添加任務(wù)<NSThread: 0x17027a340>{number = 4, name = (null)}
OperationQueue暫停與恢復(fù)
暫停與恢復(fù)只需要操作isSuspended屬性:
operationQueue.isSuspended = true
operationQueue.isSuspended = false
Operation優(yōu)先級
每一個Operation的對象都一個queuePriority屬性,表示隊列優(yōu)先級。它是一個枚舉值,有這么幾個等級可選:
public enum QueuePriority : Int {
case veryLow
case low
case normal
case high
case veryHigh
}
需要注意的是,這個優(yōu)先級并不總是起作用,不能完全保證優(yōu)先級高的任務(wù)一定先執(zhí)行,因為線程優(yōu)先級代表的是線程獲取CPU時間片的能力,高優(yōu)先級的執(zhí)行概率高,但是并不能確保優(yōu)先級高的一定先執(zhí)行。