在開始多執行緒之前,我們先來了解幾個比較容易混淆的概念。
概念
執行緒與程式
執行緒與程式之間的關係,拿公司舉例,程式相當於部門,執行緒相當於部門職員。即程式內可以有一個或多個執行緒。
併發和並行
併發指的是多個任務交替佔用CPU,並行指的是多個CPU同時執行多個任務。好比火車站買票,併發指的是一個視窗有多人排隊買票,而並行指的是多個視窗有多人排隊買票。
同步和非同步
同步指在執行一個函式時,如果這個函式沒有執行完畢,那麼下一個函式便不能執行。非同步指在執行一個函式時,不必等到這個函式執行完畢,便可開始執行下一個函式。
GCD
Swift3之後,GCD的Api有很大的調整,從原來的C語言風格的函式呼叫,變為物件導向的封裝,使用起來更加舒服,靈活性更高。
同步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.sync {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
0
1
2
3
4
10
11
12
13
14
複製程式碼
從結果可以看出佇列同步操作時,當程式在進行佇列任務時,主執行緒的操作並不會被執行,這是由於當程式在執行同步操作時,會阻塞執行緒,所以需要等待佇列任務執行完畢,程式才可以繼續執行。
非同步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.async {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
10
0
11
1
12
2
13
3
14
4
複製程式碼
從結果可以看出佇列非同步操作時,當程式在執行佇列任務時,不必等待佇列任務開始執行,便可執行主執行緒的操作。與同步執行相比,非同步佇列並不會阻塞主執行緒,當主執行緒空閒時,便可執行別的任務。
QoS 優先順序
在實際開發中,我們需要對任務分類,比如UI的顯示和互動操作等,屬於優先順序比較高的,有些不著急操作的,比如快取操作、使用者習慣收集等,相對來說優先順序比較低。
在GCD中,我們使用佇列和優先順序劃分任務,以達到更好的使用者體驗,選擇合適的優先順序,可以更好的分配CPU的資源。
GCD內採用DispatchQoS結構體,如果沒有指定QoS,會使用default
。
以下等級由高到低。
public struct DispatchQoS : Equatable {
public static let userInteractive: DispatchQoS //使用者互動級別,需要在極快時間內完成的,例如UI的顯示
public static let userInitiated: DispatchQoS //使用者發起,需要在很快時間內完成的,例如使用者的點選事件、以及使用者的手勢
。
public static let `default`: DispatchQoS //系統預設的優先順序,
public static let utility: DispatchQoS //實用級別,不需要很快完成的任務
public static let background: DispatchQoS //使用者無法感知,比較耗時的一些操作
public static let unspecified: DispatchQoS
}
複製程式碼
以下通過兩個例子來具體看一下優先順序的使用。
相同優先順序
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 5..<10 {
print(i)
}
}
queue2.async {
for i in 0..<5 {
print(i)
}
}
output:
0
5
1
6
2
7
3
8
4
9
複製程式碼
從結果可見,優先順序相同時,兩個佇列是交替執行的。
不同優先順序
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .default)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 0..<5 {
print(i)
}
}
queue2.async {
for i in 5..<10 {
print(i)
}
}
output:
0
5
1
2
3
4
6
7
8
9
複製程式碼
從結果可見,交替輸出,CPU會把更多的資源優先分配給優先順序高的佇列,等到CPU空閒之後才會分配資源給優先順序低的佇列。
主佇列預設使用擁有最高優先順序,即userInteractive
,所以慎用這一優先順序,否則極有可能會影響使用者體驗。
一些不需要使用者感知的操作,例如快取等,使用utility
即可
序列佇列
在建立佇列時,不指定佇列型別時,預設為序列佇列。
let queue = DispatchQueue(label: "com.ffib.blog.initiallyInactive.queue", qos: .utility)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
複製程式碼
從結果可見佇列執行結果,是按任務新增的順序,依次執行。
並行佇列
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes: .concurrent)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
5
0
10
1
2
3
11
4
6
12
7
13
8
14
9
複製程式碼
從結果可見,所有任務是以並行的狀態執行的。另外在設定attributes
引數時,引數還有另一個列舉值initiallyInactive
,表示的任務不會自動執行,需要程式設計師去手動觸發。如果不設定,預設是新增完任務後,自動執行。
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility,
attributes: .initiallyInactive)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
//需要呼叫activate,啟用佇列。
queue.activate()
output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
複製程式碼
從結果可見,只是把自動執行變為手動觸發,執行結果沒變,新增這一屬性帶來了,更多的靈活性,可以自由的決定執行的時機。
再來看看並行佇列如何設定這一列舉值。
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes:
[.concurrent, .initiallyInactive])
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
queue.activate()
output:
10
0
5
11
1
6
12
2
7
13
3
8
14
4
9
複製程式碼
延時執行
GCD提供了任務延時執行的方法,通過對已建立的佇列,呼叫延時任務的函式即可。其中時間以DispatchTimeInterval
設定,GCD內跟時間引數有關係的引數都是通過這一列舉來設定。
public enum DispatchTimeInterval : Equatable {
case seconds(Int) //秒
case milliseconds(Int) //毫秒
case microseconds(Int) //微妙
case nanoseconds(Int) //納秒
case never
}
複製程式碼
在設定呼叫函式時,asyncAfter
有兩個及其相同的方法,不同的地方在於引數名有所不同,參照Stack Overflow的解釋。
wallDeadline 和 deadline,當系統睡眠後,wallDeadline會繼續,但是deadline會被掛起。例如:設定引數為60分鐘,當系統睡眠50分鐘,wallDeadline會在系統醒來之後10分鐘執行,而deadline會在系統醒來之後60分鐘執行。
let queue = DispatchQueue(label: "com.ffib.blog.after.queue")
let time = DispatchTimeInterval.seconds(5)
queue.asyncAfter(wallDeadline: .now() + time) {
print("wall dead line done")
}
queue.asyncAfter(deadline: .now() + time) {
print("dead line done")
}
複製程式碼
DispatchGroup
如果想等到所有的佇列的任務執行完畢再進行某些操作時,可以使用DispatchGroup
來完成。
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
for i in 0..<10 {
print(i)
}
}
queue2.async(group: group) {
for i in 10..<20 {
print(i)
}
}
//group內所有執行緒的任務執行完畢
group.notify(queue: DispatchQueue.main) {
print("done")
}
output:
5
0
6
1
7
2
8
3
9
4
done
複製程式碼
如果想等待某一佇列先執行完畢再執行其他佇列可以使用wait
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
for i in 0..<10 {
print(i)
}
}
queue2.async(group: group) {
for i in 10..<20 {
print(i)
}
}
group.wait()
//group內所有執行緒的任務執行完畢
group.notify(queue: DispatchQueue.main) {
print("done")
}
output:
0
1
2
3
4
5
6
7
8
9
done
複製程式碼
為防止佇列執行任務時出現阻塞,導致執行緒鎖死,可以設定超時時間。
group.wait(timeout: <#T##DispatchTime#>)
group.wait(wallTimeout: <#T##DispatchWallTime#>)
複製程式碼
DispatchWorkItem
Swift3新增的api,可以通過此api設定佇列執行的任務。先看看簡單應用吧。通過DispatchWorkItem
初始化閉包。
let workItem = DispatchWorkItem {
for i in 0..<10 {
print(i)
}
}
複製程式碼
呼叫一共分兩種情況,第一種是通過呼叫perform()
,自動響應閉包。
DispatchQueue.global().async {
workItem.perform()
}
複製程式碼
第二種是作為引數傳給async
方法。
DispatchQueue.global().async(execute: workItem)
複製程式碼
接下來我們來看看DispatchWorkItem
的內部都有些什麼方法和屬性。
init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
block: @escaping () -> Void)
複製程式碼
從初始化方法開始,DispatchWorkItem
也可以設定優先順序,另外還有個引數DispatchWorkItemFlags
,來看看DispatchWorkItemFlags
的內部組成。
public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {
public static let barrier: DispatchWorkItemFlags
public static let detached: DispatchWorkItemFlags
public static let assignCurrentContext: DispatchWorkItemFlags
public static let noQoS: DispatchWorkItemFlags
public static let inheritQoS: DispatchWorkItemFlags
public static let enforceQoS: DispatchWorkItemFlags
}
複製程式碼
DispatchWorkItemFlags
主要分為兩部分:
- 覆蓋
- noQoS 沒有優先順序
- inheritQoS 繼承Queue的優先順序
- enforceQoS 覆蓋Queue的優先順序
- 執行情況
- barrier
- detached
- assignCurrentContext
執行情況會在下文會具體描述,先在這留個坑。
先來看看設定優先順序,會對任務執行有什麼影響。
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem(qos: .utility) {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
7
8
9
1
2
3
4
複製程式碼
由結果可見即使設定了DispatchWorkItem
僅僅只設定了優先順序並不會對任務執行順序有任何影響。
接下來,再來設定DispatchWorkItemFlags
試試
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated, flags: .enforceQoS) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
1
7
2
8
3
9
4
複製程式碼
設定enforceQoS
,使優先順序強制覆蓋queue的優先順序,所以兩個佇列呈交替執行狀態,變為同一優先順序。
DispatchWorkItem
也有wait
和notify
方法,和DispatchGroup
用法相同。
DispatchSemaphore
如果你想同步執行一個非同步佇列任務,可以使用訊號量。
wait()
會使訊號量減一,如果訊號量大於1則會返回.success
,否則返回timeout
(超時),也可以設定超時時間。
func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult
複製程式碼
signal()
會使訊號量加一,返回當前訊號量。
func signal() -> Int
複製程式碼
下面通過例項來看看具體的使用。
先看看不使用訊號量時,在檔案非同步寫入會發生什麼。
//初始化訊號量為1
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let fileManager = FileManager.default
let path = NSHomeDirectory() + "/test.txt"
print(path)
fileManager.createFile(atPath: path, contents: nil, attributes: nil)
//迴圈寫入,預期結果為test4
for i in 0..<5 {
queue.async {
do {
try "test\(i)".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
}catch {
print(error)
}
semaphore.signal()
}
}
}
複製程式碼
發現寫入的結果根本不是我們想要的。此時再使用訊號量試試。
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let fileManager = FileManager.default
let path = NSHomeDirectory() + "/test.txt"
print(path)
fileManager.createFile(atPath: path, contents: nil, attributes: nil)
for i in 0..<5 {
//.distantFuture代表永遠
if semaphore.wait(wallTimeout: .distantFuture) == .success {
queue.async {
do {
print(i)
try "test\(i)".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
}catch {
print(error)
}
semaphore.signal()
}
}
}
複製程式碼
寫入的結果符合預期效果,我們來看下
for
迴圈裡都發生了什麼。第一遍迴圈遇到wait
時,此時訊號量為1,大於0,所以if
判斷為true
,進行寫入操作;當第二遍迴圈遇到wait
時,發現訊號量為0,此時就會鎖死執行緒,直到上一遍迴圈的寫入操作完成,呼叫signal()
方法,訊號量加一,才會執行寫入操作,迴圈以上操作。好奇的同學,可以加上sleep(1)
,然後開啟資料夾,會發現test.txt
檔案從test1
不斷加1變為test4
。(ps:寫入檔案的方式略顯粗糙,不過這不是本文討論的重點,僅用以測試DispatchSemaphore
)DispatchSemaphore
還有另外一個用法,可以限制佇列的最大併發量,通過前面所說的wait()
訊號量減一,signal()
訊號量加一,來完成此操作,正如上文所述例子,其實達到的效果就是最大併發量為一。
如果使用過NSOperationQueue
的同學,應該知道maxConcurrentOperationCount
,效果是類似的。
DispatchWorkItemFlags
前面留了個DispatchWorkItemFlags
的坑,現在來具體看看。
barrier
可以理解為隔離,還是以檔案讀寫為例,在讀取檔案時,可以非同步訪問,但是如果突然出現了非同步寫入操作,我們想要達到的效果是在進行寫入操作的時候,使讀取操作暫停,直到寫入操作結束,再繼續進行讀取操作,以保證讀取操作獲取的是檔案的最新內容。
以上文中的test.txt
檔案為例,預期結果是:在寫入操作之前,讀取到的內容是test4
;在寫入操作之後,讀取到的內容是done
(即寫入的內容)。
先看看不使用barrier
的結果。
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let path = NSHomeDirectory() + "/test.txt"
print(path)
let readWorkItem = DispatchWorkItem {
do {
let str = try String(contentsOfFile: path, encoding: .utf8)
print(str)
}catch {
print(error)
}
sleep(1)
}
let writeWorkItem = DispatchWorkItem(flags: []) {
do {
try "done".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
print("write")
}catch {
print(error)
}
sleep(1)
}
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
queue.async(execute: writeWorkItem)
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
output:
test4
test4
test4
test4
test4
test4
write
複製程式碼
結果不是我們想要的。再來看看加了barrier
之後的效果。
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let path = NSHomeDirectory() + "/test.txt"
print(path)
let readWorkItem = DispatchWorkItem {
do {
let str = try String(contentsOfFile: path, encoding: .utf8)
print(str)
}catch {
print(error)
}
}
let writeWorkItem = DispatchWorkItem(flags: .barrier) {
do {
try "done".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
print("write")
}catch {
print(error)
}
}
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
queue.async(execute: writeWorkItem)
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
output:
test4
test4
test4
write
done
done
done
複製程式碼
結果符合預期的想法,barrier
主要用於讀寫隔離,以保證寫入的時候,不被讀取。