目錄
<a name="前言"></a>前言
在日常app開發中多線程的使用是難免的,既然躲不過,干嘛不好好的享受? 本文將對GCD進行全面的解析。
<a name="為什么選擇GCD?"></a>為什么選擇GCD?
它是蘋果在 iOS 4 時推出的,為多核的并行運算提出的, 以c語言編寫的解決方案。高效,自動管理是它的優點。
<a name="串行隊列、并行隊列、同步、異步"></a>串行隊列、并行隊列、同步、異步
串行隊列:放到串行隊列的任務,GCD 會FIFO(先進先出)地取出來一個,執行一個,然后取下一個,這樣一個一個的執行。
并行隊列
- 為異步執行時:放到并行隊列的任務,GCD 也會FIFO的取出來,但不同的是,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快,忽略不計,看起來,所有的任務都是一起執行的。不過需要注意,GCD 會根據系統資源控制并行的數量,所以如果任務很多,它并不會讓所有任務同時執行。
- 為同步時:和串行隊列基本一致。
同步:會先阻塞當前線程,直到任務完成。
異步:會另外開線程處理,不會阻塞當前線程。
串行情況下的 同步和異步
func testFive() {
//串行隊列
let qe = DispatchQueue.init(label: "concurrentQueue")
print("當前線程",Thread.current)
//同步
qe.sync {
for i in 0..<5 {
print("?? - \(i)",Thread.current)
}
}
qe.sync {
for i in 0..<5 {
print("?? - \(i)",Thread.current)
}
}
//異步
qe.async {
for i in 0..<5 {
print("???? - \(i)",Thread.current)
}
}
qe.async {
for i in 0..<5 {
print("???? - \(i)",Thread.current)
}
}
}
當前線程 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 0 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 1 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 2 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 3 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 4 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 0 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 1 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 2 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 3 <NSThread: 0x60800006d540>{number = 1, name = main}
?? - 4 <NSThread: 0x60800006d540>{number = 1, name = main}
???? - 0 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 1 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 2 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 3 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 4 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 0 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 1 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 2 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 3 <NSThread: 0x600000078f40>{number = 3, name = (null)}
???? - 4 <NSThread: 0x600000078f40>{number = 3, name = (null)}
結論:可以看出,在串行情況下,不管是異步還是同步都是一個個按照順序執行。唯一的區別就是異步單獨開了個線程
并行情況下的 同步和異步
func testFive() {
//其實就是初始化函數里,多了個concurrent 的參數
let qe = DispatchQueue.init(label: "concurrentQueue", attributes:.concurrent)
print("當前線程",Thread.current)
//同步
qe.sync {
for i in 0..<5 {
print("?? - \(i)",Thread.current)
}
}
qe.sync {
for i in 0..<5 {
print("?? - \(i)",Thread.current)
}
}
//異步
qe.async {
for i in 0..<5 {
print("???? - \(i)",Thread.current)
}
}
qe.async {
for i in 0..<5 {
print("???? - \(i)",Thread.current)
}
}
}
當前線程 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 0 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 1 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 2 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 3 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 4 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 0 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 1 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 2 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 3 <NSThread: 0x60000006b380>{number = 1, name = main}
?? - 4 <NSThread: 0x60000006b380>{number = 1, name = main}
???? - 0 <NSThread: 0x608000078640>{number = 3, name = (null)}
???? - 0 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
???? - 1 <NSThread: 0x608000078640>{number = 3, name = (null)}
???? - 1 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
???? - 2 <NSThread: 0x608000078640>{number = 3, name = (null)}
???? - 2 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
???? - 3 <NSThread: 0x608000078640>{number = 3, name = (null)}
???? - 3 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
???? - 4 <NSThread: 0x608000078640>{number = 3, name = (null)}
???? - 4 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
結論:可以看出,在并行情況下,同步時和串行一致。異步則會開多個線程并行執行。
當然,如果你還是有點暈的話可以看下面這個表格,一般情況都可以用下面的表格解釋(例外看下面的注意點!)
同步執行 | 異步執行 | |
---|---|---|
串行隊列 | 當前線程,一個一個執行 | 其他線程,一個一個執行 |
并行隊列 | 當前線程,一個一個執行 | 開很多線程,一起執行 |
注意點:經過測試,得到的經驗:(1)主線程中的其他隊列異步都能分出線程(系統線程資源沒用完前),(2)分線程中的其他隊列異步不一定分出線程;(3)主線程中的主隊列異步不能分出線程。
<a name="線程死鎖解析"></a>線程死鎖解析
一、串行隊列
- 1、主線程死鎖
func syncFirst() {
print("MainQueue-Task1",Thread.current)
DispatchQueue.main.sync {
print("MainQueue-Task2-sync",Thread.current)
}
print("MainQueue-Task3",Thread.current)
}
輸出:1
MainQueue-Task1 <NSThread: 0x60000006e640>{number = 1, name = main}
解析:
在主隊列存在3個任務(任務1,同步任務2,任務3),按照正常的隊列的順序執行順序應該為
任務1-->同步任務2-->任務3;但是因為同步任務2的存在,導致主線程阻塞,且同步任務2被抽出重新入隊列,于是同步任務2,在主隊列中排在了任務3的后面。那么問題來了,現在有兩條線 1: 任務1-->同步任務2-->任務3 // 2: 任務1-->任務3-->同步任務2 于是主線程就會因為互相等待不知道先執行哪個而完全阻塞。
如下圖所示
- 2、其他隊列的阻塞
func syncSec() {
let queue = DispatchQueue.init(label: "MyQueue")
print("MainQueue-Task1",Thread.current)
queue.async {
print("MyQueue-Task2",Thread.current)
queue.sync(execute: {
print("MyQueue-Task3",Thread.current)
})
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
輸出:1 -(2/5)
MainQueue-Task1 <NSThread: 0x608000071400>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x608000071400>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x60000007d200>{number = 3, name = (null)}
解析:
在主隊列存在3個任務(任務1,異步任務,任務5),那么毫無疑問任務1最先執行,因中間隔著是異步線程,故任務5可以和異步線程同時開始執行;MyQueue串行隊列中存在3個任務(同步任務2,同步任務,同步任務),任務2先執行,然后遇到了同步操作,那么還是和上個問題一樣,同步任務3和任務4之間相互等待,造成 線程<NSThread: 0x60000007d200>的完全阻塞,程序停止;
如下圖所示
- 3、錯覺:只要串行隊列中有同步操作,就會立馬死鎖?
看完第一個和第二個的例子是不是有一種錯覺:只要串行隊列中有同步操作,就會立馬死鎖?其實不然,下面的例子就證明了有串行同步操作也是可以的。
func syncThree() {
let qe = DispatchQueue.init(label: "MyQueue")
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
print("MyQueue-Task3",Thread.current)
}
print("MainQueue-Task4",Thread.current)
}
輸出:1-2-3 ,且沒有推出程序
MainQueue-Task1 <NSThread: 0x600000079000>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x600000079000>{number = 1, name = main}
MyQueue-Task3 <NSThread: 0x600000079000>{number = 1, name = main}
MainQueue-Task4 <NSThread: 0x600000079000>{number = 1, name = main}
解析:
主隊列中3個任務(任務1,同步任務,任務4),MyQueue隊列中兩個任務(任務2,任務3)。主線程首選執行主隊列中的任務1,然后遇到了同步任務,關鍵來了!因同步任務是MyQueue的全部任務集合,故不會和主隊列進行沖突,而是按照官方描述的,遇到同步任務要執行完任務中的事情,于是任務2和任務3相繼被執行,之后主隊列的任務4出隊列被執行! 順序為:1->2->3->4。
如下圖所示
- 4、串行隊列下到底怎樣的情況下才會造成死鎖?
func syncFour() {
let qe = DispatchQueue.init(label: "MyQueue")//, attributes:.concurrent)
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
qe.sync {
print("MyQueue-Task3",Thread.current)
}
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
輸出 1-2
MainQueue-Task1 <NSThread: 0x60800006f240>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x60800006f240>{number = 1, name = main}
解析:
主隊列中3個任務(任務1,同步任務,任務5),MyQueue隊列中三個任務(任務2,同步任務3,任務4)。
主線程首先執行任務1,之后遇到MyQueue的同步任務,跳到MyQueue中執行任務2,又遇到了MyQueue的同步任務,那么同步任務中的任務3被推入隊列中,導致任務3跑到任務4的后面,導致了任務3和任務4的相互等待,造成死鎖!
我們回到第一個例子的代碼中再思考一下,和這段代碼有沒有共同點??其實我們可以理解為下面這段偽代碼
//偽代碼,為了說明
MainQueue.sync{
viewDidload{
....
syncFirst();
....
}
}
func syncFirst() {
print("MainQueue-Task1",Thread.current)
DispatchQueue.main.sync {
print("MainQueue-Task2-sync",Thread.current)
}
print("MainQueue-Task3",Thread.current)
}
是不是明白了什么?整個主線程已經是在串行同步的條件下了,
所以我么可以總結一下串行隊列的死鎖情況:串行隊列中有屬于自身隊列的同步操作,就會立馬死鎖!或者說 任何一個串行隊列,不能添加兩個本隊列的同步操作!
如下圖所示
二、并行隊列
- 1、并行隊列下的同步
func syncFive() {
let qe = DispatchQueue.init(label: "MyQueue", attributes:.concurrent)
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
qe.sync {
print("MyQueue-Task3",Thread.current)
}
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
輸出:1-2-3-4-5
MainQueue-Task1 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task3 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task4 <NSThread: 0x608000076480>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x608000076480>{number = 1, name = main}
解析
主隊列中3個任務(任務1,同步任務,任務5),MyQueue隊列中三個任務(任務2,同步任務3,任務4)。
主線程首先執行任務1,遇到MyQueue的同步任務跳到MyQueue隊列中,執行任務2,此時遇到了MyQueue自己隊列的同步任務3,因是并行情況所有所以直接執行(ps:至于為什么直接過去了,我還沒找到原理般的解釋,先相信知乎上這篇文章上第一個回答所說的,如果大家知道原理,可以告訴我。)
執行完成后再回到主隊列,執行任務5。
如下圖所示
- 2、 并行下也是有可能出現死鎖的,別大意
func testThree() {
// 全局線程
let queueGlobal = DispatchQueue.global()
// 主線程
let queueMain = DispatchQueue.main
print("MainQueue-Task1",Thread.current)
//全局異步
queueGlobal.async {
print("GlobalQueue-Task2 ",Thread.current)
//主線程同步,因主線程阻塞,block內容和while循環相互等待
queueMain.sync(execute: {
print("GlobalQueue-Task3 ",Thread.current)
})
print("GlobalQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
sleep(3);
print("MainQueue-Task6")
}
輸出 1-(5/2)-6-3-4
MainQueue-Task1 <NSThread: 0x60000007d4c0>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x60000007d4c0>{number = 1, name = main}
GlobalQueue-Task2 <NSThread: 0x608000269680>{number = 3, name = (null)}
MainQueue-Task6
GlobalQueue-Task3 <NSThread: 0x60000007d4c0>{number = 1, name = main}
GlobalQueue-Task4 <NSThread: 0x608000269680>{number = 3, name = (null)}
解析
看圖就知道這個比較麻煩!主隊列中6個任務(任務1,異步任務,任務5,休眠任務,任務6,和后面入對的任務3),GlobleQueue隊列中三個任務(任務2,同步任務,任務4)。主線程執行任務1,遇到異步則分出線程執行任務2,同時主線程繼續執行任務5,所以任務2和5位置不一定。現在有兩條線程,主線程上進入了3秒休眠。分線程上因遇到主線程的同步任務,主線程為串行同步隊列,故需要把任務3加入到主線程的隊尾,在任務6之后。休眠結束后,主線程繼續任務6和任務3,任務3執行完之后,同步任務才得以完成,所以任務4才能執行。有點繞,要消化一下!
如下圖所示
<a name="DispatchQueue的使用"></a>DispatchQueue的使用
初始化
let queue = DispatchQueue.init(label:String,qos:DispatchQoS,attributes:DispatchQueue.Attributes,autoreleaseFrequency:DispatchQueue.AutoreleaseFrequency, target: DispatchQueue?)
屬性
- label 隊列的標識 (required字段,下面都是option字段)
- qos (quality of server) 服務質量,也可以說是優先級,優先級高的得到的資源和權限就越高!下面為從高到低的優先級
- userInteractive
- userInitiated
- default
- utility
- background
- unspecified
- attributes
- concurrent: 并行自動模式
- initiallyInactive: 還是串行,但需要手動啟動 (如queue.activate())
- autoreleaseFrequency
- inherit 繼承
- workItem 工作項目
- never
- target 新的一個queue。注意:如果傳這個參數的話,前面的配置無效,全部按照新傳入的queue本身的配置。(親測)
<a name="Dispatchgroup的使用"></a>Dispatchgroup的使用
初始化
let group = DispatchGroup.init()
實戰
下面的一段代碼很好的解決了多個網絡請求并行的問題。
func testGroup() {
let group = DispatchGroup.init()
let queueOne = DispatchQueue.init(label: "queueOne",attributes:.concurrent)
let firstTime = NSDate.init().timeIntervalSince1970
print("主線程",Thread.current)
print("開始時間",firstTime)
//這里和下面的三段模擬日常的網絡請求
group.enter()
queueOne.async(group: group) {
sleep(1) //模擬請求延遲
print("1結束")
group.leave()
}
group.enter()
queueOne.async(group: group) {
sleep(2)
print("2結束")
group.leave()
}
group.enter()
queueOne.async(group: group) {
sleep(3)
print("3結束")
group.leave()
}
group.notify(queue: DispatchQueue.main) {
let secTime = NSDate.init().timeIntervalSince1970
print("所有異步執行結束時間",secTime)
print("主線程",Thread.current)
}
}
輸出:
主線程 <NSThread: 0x60000006e800>{number = 1, name = main}
開始時間 1487831647.5901
1結束
2結束
3結束
所有異步執行結束時間 1487831650.66429
主線程 <NSThread: 0x60000006e800>{number = 1, name = main}
解析
首先先創建一個group和一個并行的queue;然后往group里添加三段 并行queue異步的模擬網絡請求,分別延遲1s,2s,3s。看輸出你就可以看出,整個三段請求的時間是按照 請求時間最大的一段來決定的,所以是3s的時間。等所有的請求都完成之后,就會執行group的notify回調,傳人主線程,就可以刷新UI拉。
注意點
enter 和 leave實際執行的次數得是1:1的,不然就會crash。所以我們平時就可以在網絡請求的成功和失敗block各放一個leave。因其要么失敗要么成功,比例還是1:1。
<a name="DispatchSourceTiemr的使用"></a>DispatchSourceTiemr的使用
初始化
(評論中有朋友遇到過 定時器不執行的問題,是因為 沒把 定時器全局化 。如:)
var timer:DispatchSourceTimer? 感謝XIAODAO同學 的建議,哈哈)
let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags.strict, queue: DispatchQueue.main)
當然也可以簡單的初始化:
let timer = DispatchSource.makeTimerSource()//默認了主線程
注意點
timer要全局定義,上面那樣局部定義初始化是不會執行的
屬性
- flags 一般不用傳
- queue 定時器運行的隊列,傳入主隊列 在主線程執行;不傳則默認在分線程
主要方法
scheduleOneshot 循環單次
-
scheduleRepeating 重復循環
兩個方法的屬性
- wallDeadline 延遲執行
- leeway 允許的誤差
- deadline 執行的時間
- interval 循環模式時的間隔時間
來看幾段代碼
單次執行
func runOnce() {
print("主線程",Thread.current)
//創建主線程下的定時器
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
//單次執行
timer?.scheduleOneshot(deadline: DispatchTime.now(), leeway: .milliseconds(10))
//執行的代碼塊
timer?.setEventHandler {
print("單次執行",Thread.current)
}
//繼續,用activate()也行
timer?.resume()
}
輸出:
主線程 <NSThread: 0x6080000740c0>{number = 1, name = main}
單次執行 <NSThread: 0x6080000740c0>{number = 1, name = main}
多次執行
func runMul() {
print("主線程",Thread.current)
創建分線程下的定時器
timer = DispatchSource.makeTimerSource()
//循環執行,馬上開始,間隔為1s,誤差允許10微秒
timer?.scheduleRepeating(deadline: DispatchTime.now(), interval: .seconds(1), leeway: .milliseconds(10))
//執行內容
timer?.setEventHandler {
print("1 second interval",Thread.current)
}
//激活
timer?.activate()
}
輸出:
主線程 <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
...
...
取消執行
func runCancel() {
print("主線程",Thread.current)
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
timer?.scheduleRepeating(deadline: DispatchTime.now(), interval: .seconds(1), leeway: .milliseconds(10))
timer?.setEventHandler {
print("1 second interval",Thread.current)
sleep(2)
print("after sleep")
}
timer?.activate()
//主隊列創建的情況下 如果在這里直接cancel(),handler里面的內容是不能執行的。如果是默認的分線程,則是可以的,至于為什么,我前面多線程的時候解釋過了哈。
//timer?.cancel()
//最好都這樣取消
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: {
self.timer?.cancel()
})
}
輸出:
主線程 <NSThread: 0x6080000644c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f000>{number = 3, name = (null)}
after sleep
<a name="延遲執行"></a>延遲執行
GCD的延遲執行一般來說可以分為兩大類
- 1、 隊列直接asyncAfter
- 2、 定時器
如下代碼所示
func testDelay() {
let queue = DispatchQueue.init(label: "myqueue")
let delayTime = DispatchTime.now() + 2.0
let delayTimeTimer = DispatchWallTime.now() + 2.0
print("before delay")
//第一種
queue.asyncAfter(deadline: delayTime) {
print("這是 asyncAfter delay")
}
//第二種
let workItem = DispatchWorkItem.init {
print("這是 asyncAfter workItem delay")
}
queue.asyncAfter(deadline: delayTime, execute: workItem)
//第三種
timer = DispatchSource.makeTimerSource()
timer?.scheduleOneshot(wallDeadline:delayTimeTimer)
timer?.setEventHandler(handler: {
print("這是 timer delay")
})
timer?.activate()
}
輸出:
before delay
這是 timer delay
這是 asyncAfter delay
這是 asyncAfter workItem delay
總結
針對本文的觀點,如有錯誤點,煩請指出!
本文引用以下文章的部分觀點:五個案例讓你明白GCD死鎖、關于iOS多線程,你看我就夠了、
作者:燈泡蟲