Swift多線程編程總結

在開始多線程以前,咱們先來了解幾個比較容易混淆的概念。程序員

概念

線程與進程

線程與進程之間的關係,拿公司舉例,進程至關於部門,線程至關於部門職員。即進程內能夠有一個或多個線程。api

併發和並行

併發指的是多個任務交替佔用CPU,並行指的是多個CPU同時執行多個任務。比如火車站買票,併發指的是一個窗口有多人排隊買票,而並行指的是多個窗口有多人排隊買票。緩存

同步和異步

同步指在執行一個函數時,若是這個函數沒有執行完畢,那麼下一個函數便不能執行。異步指在執行一個函數時,沒必要等到這個函數執行完畢,即可開始執行下一個函數。bash

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
}

複製代碼

如下經過兩個例子來具體看一下優先級的使用。async

相同優先級函數

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也有waitnotify方法,和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主要用於讀寫隔離,以保證寫入的時候,不被讀取。

相關文章
相關標籤/搜索