Dispatch ( 全稱 Grand Central Dispatch,簡稱 GCD ) 是一套由 Apple 編寫以提供讓程式碼以多核併發的方式執行應用程式的框架。
DispatchQueue
( 排程佇列 ) 就是被定義在 Dispatch 框架中,可以用來執行跟多執行緒有關操作的類。
在使用它之前,我們得先了解一下基本概念,我會先簡單介紹,後面再根據講解的內容逐步詳細介紹,目的是為了方便讀者融入。
PS:如果在閱讀時發現有任意錯誤,請指點我,感謝!
同步和非同步執行
如圖。同步和非同步的區別在於,執行緒會等待同步任務執行完成;執行緒不會等待非同步任務執行完成,就會繼續執行其他任務/操作。
閱讀指南:
本文中出現的 "任務" 是指
sync {}
和async {}
中整個程式碼塊的統稱,"操作" 則是在 "任務" 中執行的每一條指令 ( 程式碼 ) ;因為主執行緒沒有 "任務" 之說,主執行緒上執行的每一條 ( 段 ) 程式碼,都統稱為 "操作"。
序列和併發佇列
在 GCD 中,任務由**佇列 (序列或併發) **負責管理和決定其執行順序,在一條由系統自動分配的執行緒上執行。
在序列 (Serial) 佇列中執行任務時,任務會按照固定順序執行,執行完一個任務後再繼續執行下一個任務 (這意味著序列佇列同時只能執行一個任務) ;在併發 (Concurrent) 佇列中執行任務時,任務可以同時執行 ( 其實是在以極短的時間內不斷的切換執行緒執行任務 ) 。
序列和併發佇列都以 先進先出 (FIFO) 的順序執行任務,任務的執行流程如圖:
示例1 - 在序列佇列中執行同步 ( sync
) 任務
// 建立一個佇列(預設就是序列佇列,不需要額外指定引數)
let queue = DispatchQueue(label: "Serial.Queue")
print("thread: \(Thread.current)")
queue.sync {
(0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}
queue.sync {
(0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}
/**
thread: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
*/
複製程式碼
沒什麼好解釋的,結果肯定是按照正常的順序來,一個接著一個地執行。因為同步執行就是會一直等待,等到一個任務全部執行完成後,再繼續執行下一個任務。
有一點需要注意的是,主執行緒和在同步任務中 Thread,current
的列印結果相同,也就是說,佇列中的同步任務在執行時,系統給它們分配的執行緒是主執行緒,因為同步任務會讓執行緒等待它執行完,既然會等待,那就沒有再開闢執行緒的必要了。
關於主執行緒和主佇列
當應用程式啟動時,就有一條執行緒被系統建立,與此同時這條執行緒也會立刻執行,該執行緒通常叫做程式的主執行緒。
同時系統也為我們提供一個名為主佇列 ( DispatchQueue.main {}
) 的序列特殊佇列,預設我們寫的程式碼都處於主佇列中,主佇列中的所有任務都在主執行緒執行。
示例2 - 在序列佇列中執行非同步 ( async
) 任務
let queue = DispatchQueue(label: "serial.com")
print("thread: \(Thread.current)")
(0..<50).forEach {
print("main - \($0)")
// 讓執行緒休眠0.2s,目的是為了模擬耗時操作,不再贅述。
Thread.sleep(forTimeInterval: 0.2)
}
queue.async {
(0..<5).forEach {
print("rool-1 -> \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
queue.async {
(0..<5).forEach {
print("rool-2 -> \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
/**
thread: <NSThread: 0x281251fc0>{number = 1, name = main}
main - 0
main - 1
main - 2 ... 順序執行到 49
rool-1 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
*/
複製程式碼
可以看到,執行緒一定會等待它當前的操作 ( 包括讓執行緒休眠 ) 執行完後,再繼續執行 async
任務。此時任務同樣按順序執行,因為序列佇列只能執行完一個任務後再繼續執行下一個任務。
任務中 Thread.current
的列印結果都是 number = 3
,換句話說,序列佇列中的非同步任務在執行時,系統給它們開闢的執行緒是其他執行緒,並且只開闢一個,因為序列佇列同時只能執行一個任務,因此沒有開啟多條執行緒的必要。
關於讓執行緒休眠
這裡解釋一下 Thread.sleep
這個方法的作用:是讓當前執行緒暫停任何操作0.2s。
請注意我說的是當前執行緒,不要誤以為是讓整個應用程式都停止了,不是這樣的。如果當前任務所在的執行緒停止了,是不會影響到別的執行緒正在執行任務的,這點要區分清楚。
PS:也就是說,在上面同步任務中,為了測試而呼叫的 Thread.sleep
方法並沒有作用 ( 但是為了測試和驗證,依然呼叫了 ) ,因為任務都在一條執行緒上,並按照固定順序執行。
示例3 - 在序列佇列中執行非同步 ( async
) 任務 II
let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2: \(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
/**
1: <NSThread: 0x28347ed00>{number = 1, name = main}
3: <NSThread: 0x28347ed00>{number = 1, name = main}
2: <NSThread: 0x2834268c0>{number = 3, name = (null)}
5: <NSThread: 0x28347ed00>{number = 1, name = main}
4: <NSThread: 0x2834268c0>{number = 3, name = (null)}
*/
複製程式碼
這時候列印的順序並不固定,但肯定會先從 1
開始列印,列印的結果可能是:12345, 12354, 13254, 13245, 13524, 13254...
,這是為什麼?我們先來了解一些概念後再來回顧。
佇列和任務的關係
首先要解釋一下同步和非同步這兩個詞的概念,既然是同步或非同步,也能解釋為相同,或是不同,它需要一個作為參照的物件,來知道它們相對於這個物件來說到底是相同,還是不同。
那在 GCD 中,它們的參照物件就是我們的主執行緒 ( dispatchQueue.main
) 。也就是說如果是同步任務,那就在主執行緒執行;而如果是非同步任務,那就在其他執行緒執行。
這就解釋了,為什麼序列佇列在執行非同步任務時,還會開啟執行緒,所謂非同步嘛,那就是不在主執行緒執行,區別是序列佇列只會開啟一條執行緒,而併發佇列會開啟多條執行緒。
而同步任務是,甭管它是什麼佇列和任務,只要執行的是同步任務,就在主執行緒執行 。
-
非同步任務
非同步任務說:“我要開始執行任務了,快給我分配執行緒讓我執行。”
應用程式說:“好!我另外開闢執行緒出來讓你執行,等等,請問你所處的佇列是?”
非同步任務說:“序列佇列。”
應用程式說:“既然是序列佇列,而序列佇列中的所有任務都會按照固定順序執行,只能執行完一個任務後再繼續執行下一個任務 ( 這意味著序列佇列同時只能執行一個任務 ) ,那我就只給你分配一條執行緒吧!你佇列中的所有任務、包括你,都在這條執行緒上順序執行。”
非同步任務說:“那如果我處在併發佇列中呢?”
應用程式說:“如果是在併發佇列中,那佇列中的所有任務可以同時執行,我會給你分配多條執行緒,讓每個任務可以在不同的執行緒上同時執行。”
-
同步任務
同步任務說:“我要開始執行任務了,快給我分配執行緒讓我執行。”
應用程式說:“既然是同步任務那就相當於在主執行緒執行,那我就給你主執行緒來執行吧!”
同步任務說:“我的待遇太差了。”
任務和執行緒的關係
任務只有兩種,同步任務和非同步任務,無論同步任務是處在什麼佇列中,它都會讓當前正在執行的執行緒等待它執行完成,例如:
// 當前執行緒執行列印 main-1 的操作
print("main-1")
// 執行緒執行到這裡發現遇到一個 sync 任務,就會在此等待,
// 直到 sync 任務執行完成,才會繼續執行其他操作。
//
// 序列或併發佇列
queue.sync {
(0..<10).forEach {
print("sync \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
// 等待!執行緒等待 sync 執行完後,再繼續執行列印 main-2 的操作。
print("main-2")
/**
main-1
sync 0: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 1: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 2: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 2 ...9
main-2
*/
複製程式碼
而如果是非同步任務,不管它處在什麼佇列中,當前執行緒都不會等待它執行完成,例如:
// 當前執行緒執行列印 main-1 的操作
print("main-1")
// 執行緒執行到這裡發現遇到一個 async 任務,
// 那麼執行緒不會等待它執行完成,就會繼續執行其他操作。
//
// 序列或併發佇列
queue.async {
(0..<20).forEach { print("async \($0)") }
}
// 開闢執行緒的時間大約是90微妙,加上迴圈的準備以及列印時間,
// 這裡給它200微妙,測試async任務中的執行緒和當前執行緒之間的執行順序。
Thread.sleep(forTimeInterval: 0.0002000)
// 不會等待!執行緒不會等待 async 執行完成就會執行列印 main-2 的操作
print("main-2")
複製程式碼
列印的結果可能稍有不同,但是肯定先從 main-1
開始列印。雖然 main-2
是執行在 async
後面的,async
也會先執行,但是由於當前執行緒不等待它執行完成的機制,所以它在執行到某一刻時如果到了執行緒需要列印 main-2
的時間,就會執行列印 main-2
的操作。也有可能是,main-2
先執行,然後等到了某一時刻再執行 async
中的任務 ( 開闢執行緒需要時間 ) 。
也就是說,這裡當前執行緒和 async
任務中的執行緒在執行時是不阻塞對方的 ( 互不等待 ) ,本次執行結果如下:
/**
main-1
async 0
async 1
async 2
main-2
async 3
async 4
async 5
...
*/
複製程式碼
PS:我是怎麼知道開闢執行緒的時間大約是 90 微妙的?因為我看了執行緒成本中的描述。
回顧
這就能解釋之前示例中的執行順序了,再來回顧一下:
let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2-\(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
複製程式碼
雖然執行順序不固定,但還是有一定的規律可循的,因為是序列佇列,所以在主執行緒中 1, 3, 5
一定按順序執行,而在 async
執行緒中 2, 4
也一定按順序執行。
示例4 - 序列佇列死鎖
首先,併發佇列不會出現死鎖的情況;其次,在序列佇列中,只有 sync { sync {} }
和 async { sync {} }
會出現死鎖,內部的 sync closure 永遠不會被執行,並且程式會崩潰,例如:
queue.sync {
print("1")
queue.sync { print("2") }
print("3")
}
// Prints "1"
queue.async {
print("1")
queue.sync { print("2") }
print("3")
}
// Prints "1"
複製程式碼
仔細觀察上面的程式碼就會發現,只有內部套用 sync {}
的情況下才會死鎖,那使用 sync
( 同步 ) 意味著什麼呢?這意味著,當前執行緒會等待同步任務執行完成。可問題是,這個 sync
任務是巢狀在另一個任務裡面的 ( sync { sync {} }
) ,那這裡就有兩個任務了。
由於序列佇列是執行完當前任務後,再繼續執行下一個任務。放到這裡就是,內部的 sync {}
想要執行的話,它必須要等待外部的 sync {}
執行完成,那外部的 sync {}
能不能執行完成呢?由於這個內部任務是同步的,它會阻塞當前正在執行外部 sync {}
的執行緒,讓當前執行緒等待它 ( 內部 sync {}
) 執行完成,可問題是外部的 sync {}
完成不了的話,內部的 sync {}
也無法執行,結果就是一直等待,誰都無法繼續執行,造成死鎖。
既然執行緒會等待內部的同步任務執行完成,又限制序列佇列同時只能執行一個任務,那在外部的 sync {}
沒有執行完成之前,內部的 sync {}
永遠不能執行,而外部執行緒在等待內部 sync {}
執行完成的條件下,導致外部的 sync {}
也無法執行完成。
總結:因為序列佇列同時只能執行一個任務,就意味著無論如何,執行緒只能先執行完當前任務後,再繼續執行下一個任務。而同步任務的特點是,會讓執行緒等待它執行完成。那問題就來了,我 ( 執行緒 ) 既不可能先去執行它,又要等待它,結果是導致外部任務永遠無法執行完成,而內部的任務也永遠無法開啟。
對於第二段程式碼 async { sync {} }
的死鎖,原理是一樣的,不要被它外部的 async {}
給迷惑了,內部的 sync {}
同樣會阻塞它的執行緒執行,阻塞的結果就是外部的 async {}
無法執行完成,內部的 sync {}
也永遠無法開啟。
至於序列佇列另外兩種任務的巢狀結構 sync { async {} }
和 async { async }
,例如:
queue.sync {
print("task-1")
queue.async {
(0..<10).forEach {
print("task-2: \($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("task-1 - end")
}
/**
1
task-1 - end
task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 2 ... 9
*/
queue.sync {
print("task-1")
queue.async {
(0..<10).forEach {
print("task-2: \($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("task-1 - end")
}
/**
1
task-1 - end
task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 2 ... 9
*/
複製程式碼
雖然已經不再死鎖,但執行的順序稍有不同,可以看到,程式是先把外部任務執行完後,再去執行內部任務。這是因為,內部的 async {}
已經不再阻塞當前執行緒,又因為序列佇列只能先把當前任務執行完後,再去執行下一個任務,那自然而然就是先把外部任務執行完後,再接著去執行內部的 async {}
任務了。
示例5 - DispatchQueue.main 特殊序列主佇列
前面說過,async
中的任務都會在其他執行緒執行,那對於主佇列中的 async
呢?在專案中我們經常呼叫的 DispatchQueue.main.asyncAfter(deadline:)
難道是在其他執行緒執行嗎?其實不是的,如果是 DispatchQueue.main
自己的佇列,那麼即使是 async
,也會在主執行緒執行,由於主佇列本身是序列佇列,也是同時只能執行一個任務,所以是,它會在處理完當前任務後,再去處理 async
中的任務,例如:
// 實際上相當於在 DispatchQueue.main.sync {} 中執行
print("1")
DispatchQueue.main.async {
(0..<10).forEach {
print("async\($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("3")
/**
1
3
async0 <NSThread: 0x6000007928c0>{number = 1, name = main}
async1 <NSThread: 0x6000007928c0>{number = 1, name = main}
async2 <NSThread: 0x6000007928c0>{number = 1, name = main}
async3 ...9
*/
複製程式碼
雖然 async
不阻塞當前執行緒執行,但是由於都在一個佇列上,DispatchQueue.main
只能先執行完當前任務後,再繼續執行下一個任務 ( async
) 。
而如果在主執行緒呼叫 DispatchQueue.main.sync {}
又會如何呢?答案是:會死鎖。其實原因很簡單,因為整個主執行緒的程式碼就相當於放在一個大的 DispatchQueue.main.sync {}
任務中,這時候如果再呼叫 DispatchQueue.main.sync {}
,結果肯定是死鎖。
還有一點需要留意,一定要在主執行緒執行和有關 UI 的操作,如果是在其他執行緒執行,例如:
queue.async { // 併發佇列
customView.backgroundColor = UIColor.blue
}
複製程式碼
很可能就會接收到一個 Main Thread Checker: UI API called on a background thread: -[UIView setBackgroundColor:]
的崩潰報告,因此主執行緒也被稱為 UI 執行緒。
示例6 - 在併發佇列中執行同步 ( sync
) 任務
let queue = DispatchQueue(label: "serial.com", attributes: .concurrent)
queue.sync {
(0..<10).forEach {
print("task-1 \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("main-1")
queue.sync {
(0..<10).forEach {
print("task-2 \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("main-2")
/**
task-1 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
main-1
task-2 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
main-2
*/
複製程式碼
使用併發佇列執行同步任務和在主執行緒執行操作並沒有區別,因為 sync
會牢牢的將當前執行緒固定住,讓執行緒等待它執行完成後才能繼續執行其他操作。這裡也能夠看到,main-1
和 main-2
分別等待 sync
執行結束後才能執行。
示例7 - 在併發佇列中執行非同步 ( async
) 任務
線上程將要執行到某個佇列的 async
時,佇列才會開始併發執行任務,執行緒不可能跨越當前正在執行的操作去啟動任務。舉個例子:
// 指定為建立併發佇列 (.concurrent)
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach {
print("main-\($0)")
Thread.sleep(forTimeInterval: 0.02)
}
queue.async { print("task-1", Thread.current) }
queue.async { print("task-2", Thread.current) }
queue.async { print("task-3", Thread.current) }
queue.async { print("task-4", Thread.current) }
queue.async { print("task-5", Thread.current) }
queue.async { print("task-6", Thread.current) }
print("main-end")
/**
main-0
main-1
main-2 ...99
task-2 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-4 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-5 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-3 <NSThread: 0x282e38800>{number = 5, name = (null)}
task-6 <NSThread: 0x282e387c0>{number = 3, name = (null)}
print("main-end")
task-1 <NSThread: 0x282e04b40>{number = 4, name = (null)}
*/
複製程式碼
因為主執行緒也是序列佇列,程式將按照順序執行,等到所有迴圈執行完成後,才會執行 queue.async
,由於是併發佇列,所有任務都會同時執行,執行順序並不固定,而最後的 main-end
可能安插在佇列中某個任務完成前後的地方。
因為在執行 main-end
之前,任務已經被佇列併發出去了。對於主執行緒來說,它完成列印 main-end
的時間是固定的,但是佇列中併發任務的執行完成的時間並不固定 ( 執行任務會消耗時間 ) 。這時主執行緒並不會等待 async
的所有任務執行結束就會繼續執行列印 main-end
的操作。
所以是,如果在執行 async
的某個時間內剛好到了主執行緒需要列印 main-end
的時間,就會執行列印 main-end
的操作,而 async
中還沒有完成的任務將會繼續執行,如圖:
可以看到,迴圈操作結束後,佇列才開始併發執行任務,列印 main-end
的操作在 queue.async
之後執行,但是由於佇列執行任務需要時間,所以 main-end
有可能在 queue.async
執行完成之前執行。
對於一條執行緒來說,它的所有操作絕對按照固定順序執行,不存在一條執行緒同時執行多個任務的情況。而我們的所謂併發,就是給每個任務開闢一條執行緒出來執行,等到有某個執行緒執行完後,就會複用這條執行緒去執行其他在佇列中還沒有開始執行的任務。
一條執行緒只負責執行它當前任務中的所有操作,至於其他執行緒被開啟後 ( 前提是不要開啟同樣的執行緒 ) ,它們就在各自的執行緒上分別獨立執行任務,互不影響。舉個例子:
假設你要跑100米,當跑到50米的時候,就會有5個人跟你一起跑,跑到終點的時候,可能是你跑得比他們都快,也有可能是他們之中的任意人跑得比你快。
那你就可以想象成那 "5個人" 就是併發中的任務 ( 同時執行) ,而 "你" 就是當前執行緒。
示例8 - 併發佇列的疑惑 - sync { sync {} }
那什麼時候會開啟同樣的執行緒呢?也就是說,假設有一條執行緒 3 在執行,那麼在這條執行緒 3 還沒有執行完成的時候,就又有一條執行緒為 3 的任務開啟了。這對於 async
任務來說,幾乎不可能 ( 我說幾乎是因為我不確定,按照我的猜測,應該不會出現這種情況 ) ,也就是說,想要開啟同樣的一條執行緒執行非同步任務,必須要等到前面的執行緒執行完後,再用這條執行緒去執行其他任務。
但是對於 sync
任務來說,在 sync
還沒執行完的時候,我可以在 sync {}
內部又開啟一個 sync {}
任務,因為 sync {}
註定在主執行緒執行 ( async
任務無法指定在哪一條執行緒執行,而是由系統自動分配 ) ,這樣一來,就有了在一條執行緒還沒有執行完的時候,就又有一條同樣的執行緒開啟執行任務了。在序列佇列中,我們已經知道,這樣做會造成死鎖,那在併發佇列中又會如何呢?例如:
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
print("sync-start")
queue.sync {
(0..<5).forEach {
print("task \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("sync-end")
}
/**
sync-start
task 0: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 1: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 2: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 3: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 4: <NSThread: 0x600003b828c0>{number = 1, name = main}
sync-end
*/
複製程式碼
我們已經看到結果,任務按照順序執行,內部 sync
會阻塞外部 sync
我們也會清楚,問題是在外部的 sync {}
還沒有執行完的時候,為什麼內部的 sync
可以執行?
首先要了解最重要的一點,那就是,為什麼在序列佇列中內部的 sync {}
無法執行?最重要的原因在於序列佇列同時只能執行一個任務,所以在它上一個任務 ( 外部 sync
) 還沒有執行完成之前,它是不能執行下一個任務 ( 內部 sync
) 的。
而併發佇列就不同了,併發佇列可以同時執行多個任務。也就是說,內部的 sync
已經不用等待外部 sync
執行完成就可以執行了。但是由於是同步任務,所以還是會等待,等待內部 sync
執行完成後,外部的 sync
繼續執行。
請注意這裡的執行和上面所說的,不存在一條執行緒同時執行多個任務的情況並不矛盾。因為在執行內部 sync
時,外部執行緒就停止操作了 ( 其實是轉去執行內部 sync
了 ) ,如果是在執行內部 sync
的同時,外部的 sync
還在繼續執行操作,那才叫同時。
因為 sync
都在一個執行緒 ( 主執行緒 ) 上,所以當你指定任務為 sync
時,主執行緒就知道接下來要去執行 sync
任務了,等執行完這個 sync
後再執行其他操作。例如,你可以把 sync
想象成是一個方法:
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
print("sync-start")
queueSync()
print("sync-end")
}
// 相當於之前的 queue.sync {}
func queueSync() {
(0..<5).forEach {
print("task \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
複製程式碼
關於先進先出 (FIFO)
對序列佇列來說,先進先出的意思很好理解,先進先出就是,先進去的一定先執行。當我們要執行一些任務時,這些任務就被儲存在它的佇列中,當執行緒進入到任務程式碼塊時,就一定會先把這個任務執行完,再將任務出列,等這個任務出列後,執行緒才能繼續去執行下一個任務。
那對於併發佇列也是一樣,當不同的執行緒同時進入到任務程式碼塊時,就一定會先把這些任務執行完,再將這些任務出列,然後這些執行緒才能繼續去執行其他任務。
示例9 - 關於併發的個數和執行緒效能
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach { i in
queue.async { print("\(i) \(Thread.current)") }
}
複製程式碼
會怎麼樣?答案是不會怎麼樣,只是會開啟很多執行緒來執行這些非同步任務。前面說過,每一個非同步任務都是在不同的執行緒上執行的,那如果同時執行很多非同步任務的話,像我們這裡,同時開啟 100 個非同步任務,難道就係統就開闢 100 個執行緒來分別執行嗎?也不是沒有可能,這取決於你的 CPU,如果在 App 執行時,系統所能承載的最大執行緒個數為 10,那就會開闢這 10 條執行緒來重複執行任務,一次執行 10 個非同步任務。
如果開闢的執行緒上限,那麼剩下的那些任務就暫時無法執行,只能等到前面那些非同步任務的執行緒執行完後,再去執行後面的非同步任務。
總之一句話就是重複利用,先執行完的去執行還沒有開始執行的,如果開闢的執行緒超出限制,那後面的任務就要等待前面的執行緒執行完才能執行。
但是如果開闢很多執行緒的話,會不會對我們的應用程式有負的影響?答案是一定的,開闢一條執行緒就要消耗一定的記憶體空間和系統資源,如果同時存在很多執行緒的話,那本身留給應用程式的記憶體就少得可憐,應用程式在執行時就會很卡,所以並不是執行緒開得越多越好,需要開發者自己平衡。
示例10 - DispatchQueue.global(_:) 全域性併發佇列
除了序列主佇列外,系統還為我們建立了一個全域性的併發佇列 ( DispatchQueue.global()
) ,如果不想自己建立併發佇列,那就用系統的 ( 我們一般也是用系統的 ) 。
DispatchQueue.global().async {
print("global async start \(Thread.current)")
DispatchQueue.global().sync {
(0..<5).forEach {
print("roop\($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("global async end \(Thread.current)")
}
/**
global async start <NSThread: 0x600002085300>{number = 3, name = (null)}
roop0 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop1 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop2 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop3 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop4 <NSThread: 0x600002085300>{number = 3, name = (null)}
global async end <NSThread: 0x600002085300>{number = 3, name = (null)}
*/
複製程式碼
和主佇列一樣,它的特殊之處在於,即使是用 sync
,任務也會在其他執行緒執行,至於它在哪一條執行緒執行,我猜測是它一定會讓執行外部 async
的這條執行緒來執行,因為 sync
就是會讓執行緒暫停執行後續操作,等到 sync
執行完後再接著執行,也就是說,在這種情況下,它只能順序執行,那似乎只要一條執行緒就足夠了,沒有必要再開闢新執行緒來執行內部的 sync
。
另外,全域性併發佇列只有一個,並不是呼叫一次系統就建立一個,經過測試,它們是相等的:
let queue1 = DispatchQueue.global()
let queue2 = DispatchQueue.global()
if queue1 == queue2 { print("相等") }
// Prints "相等"
複製程式碼
總結
在前面的示例中,有關概念都是跟隨示例引申出來的,講得不是那麼統一,在這裡就總結一下。
-
佇列
-
序列佇列 在序列佇列中執行任務時,任務按固定順序執行,只能執行完一個任務後,再繼續執行下一個任務 ( 這意味著序列佇列同時只能執行一個任務 ) 。
-
併發佇列
併發佇列可以同時執行多個任務,任務並不一定按順序執行,先執行哪幾個任務由系統自動分配決定,等到有某個任務執行完後,就將這個任務出列,然後執行緒才能繼續去執行其他任務。
-
-
任務
-
同步任務
不管是序列還是非同步佇列,只要是同步任務,就在主執行緒執行 (
DispatchQueue.global().sync
例外 ) 。同步任務會阻塞當前執行緒,讓當前執行緒只能等待它執行完畢後才能執行。
在序列佇列中,任務巢狀了
sync {}
的話會導致死鎖。 -
非同步任務
不論是序列還是非同步佇列,只要是非同步任務,就在其他執行緒執行 (
DispatchQueue.main.sync
例外 ) ,不同的是序列佇列在執行非同步任務時,只會開闢一條執行緒,而併發佇列在執行非同步任務時,可以開闢多條執行緒。非同步任務不會阻塞當前執行緒,執行緒不用等待非同步任務執行完成就可以繼續執行其他任務/操作。
非同步任務不會產生死鎖。
-