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