Swift多線程編程總結(jié)

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

概念

線程與進程

線程與進程之間的關(guān)系,拿公司舉例,進程相當于部門,線程相當于部門職員。即進程內(nèi)可以有一個或多個線程。

并發(fā)和并行

并發(fā)指的是多個任務(wù)交替占用CPU,并行指的是多個CPU同時執(zhí)行多個任務(wù)。好比火車站買票,并發(fā)指的是一個窗口有多人排隊買票,而并行指的是多個窗口有多人排隊買票。

同步和異步

同步指在執(zhí)行一個函數(shù)時,如果這個函數(shù)沒有執(zhí)行完畢,那么下一個函數(shù)便不能執(zhí)行。異步指在執(zhí)行一個函數(shù)時,不必等到這個函數(shù)執(zhí)行完畢,便可開始執(zhí)行下一個函數(shù)。

GCD

Swift3之后,GCD的Api有很大的調(diào)整,從原來的C語言風(fēng)格的函數(shù)調(diào)用,變?yōu)槊嫦驅(qū)ο蟮姆庋b,使用起來更加舒服,靈活性更高。

同步

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
復(fù)制代碼

從結(jié)果可以看出隊列同步操作時,當程序在進行隊列任務(wù)時,主線程的操作并不會被執(zhí)行,這是由于當程序在執(zhí)行同步操作時,會阻塞線程,所以需要等待隊列任務(wù)執(zhí)行完畢,程序才可以繼續(xù)執(zhí)行。

異步

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
復(fù)制代碼

從結(jié)果可以看出隊列異步操作時,當程序在執(zhí)行隊列任務(wù)時,不必等待隊列任務(wù)開始執(zhí)行,便可執(zhí)行主線程的操作。與同步執(zhí)行相比,異步隊列并不會阻塞主線程,當主線程空閑時,便可執(zhí)行別的任務(wù)。

QoS 優(yōu)先級

在實際開發(fā)中,我們需要對任務(wù)分類,比如UI的顯示和交互操作等,屬于優(yōu)先級比較高的,有些不著急操作的,比如緩存操作、用戶習(xí)慣收集等,相對來說優(yōu)先級比較低。
在GCD中,我們使用隊列和優(yōu)先級劃分任務(wù),以達到更好的用戶體驗,選擇合適的優(yōu)先級,可以更好的分配CPU的資源。
GCD內(nèi)采用DispatchQoS結(jié)構(gòu)體,如果沒有指定QoS,會使用default。 以下等級由高到低。

public struct DispatchQoS : Equatable {

     public static let userInteractive: DispatchQoS //用戶交互級別,需要在極快時間內(nèi)完成的,例如UI的顯示

     public static let userInitiated: DispatchQoS  //用戶發(fā)起,需要在很快時間內(nèi)完成的,例如用戶的點擊事件、以及用戶的手勢
     。
     public static let `default`: DispatchQoS  //系統(tǒng)默認的優(yōu)先級,

     public static let utility: DispatchQoS   //實用級別,不需要很快完成的任務(wù)

     public static let background: DispatchQoS  //用戶無法感知,比較耗時的一些操作

     public static let unspecified: DispatchQoS
}

復(fù)制代碼

以下通過兩個例子來具體看一下優(yōu)先級的使用。

相同優(yōu)先級

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
復(fù)制代碼

從結(jié)果可見,優(yōu)先級相同時,兩個隊列是交替執(zhí)行的。

不同優(yōu)先級

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
復(fù)制代碼

從結(jié)果可見,交替輸出,CPU會把更多的資源優(yōu)先分配給優(yōu)先級高的隊列,等到CPU空閑之后才會分配資源給優(yōu)先級低的隊列。

主隊列默認使用擁有最高優(yōu)先級,即userInteractive,所以慎用這一優(yōu)先級,否則極有可能會影響用戶體驗。
一些不需要用戶感知的操作,例如緩存等,使用utility即可

串行隊列

在創(chuàng)建隊列時,不指定隊列類型時,默認為串行隊列。

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
復(fù)制代碼

從結(jié)果可見隊列執(zhí)行結(jié)果,是按任務(wù)添加的順序,依次執(zhí)行。

并行隊列

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

復(fù)制代碼

從結(jié)果可見,所有任務(wù)是以并行的狀態(tài)執(zhí)行的。另外在設(shè)置attributes參數(shù)時,參數(shù)還有另一個枚舉值initiallyInactive,表示的任務(wù)不會自動執(zhí)行,需要程序員去手動觸發(fā)。如果不設(shè)置,默認是添加完任務(wù)后,自動執(zhí)行。


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)
    }
}

//需要調(diào)用activate,激活隊列。
queue.activate()

output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
復(fù)制代碼

從結(jié)果可見,只是把自動執(zhí)行變?yōu)槭謩佑|發(fā),執(zhí)行結(jié)果沒變,添加這一屬性帶來了,更多的靈活性,可以自由的決定執(zhí)行的時機。
再來看看并行隊列如何設(shè)置這一枚舉值。

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
復(fù)制代碼

延時執(zhí)行

GCD提供了任務(wù)延時執(zhí)行的方法,通過對已創(chuàng)建的隊列,調(diào)用延時任務(wù)的函數(shù)即可。其中時間以DispatchTimeInterval設(shè)置,GCD內(nèi)跟時間參數(shù)有關(guān)系的參數(shù)都是通過這一枚舉來設(shè)置。

public enum DispatchTimeInterval : Equatable {

    case seconds(Int)     //秒

    case milliseconds(Int) //毫秒

    case microseconds(Int) //微妙

    case nanoseconds(Int)  //納秒

    case never
}
復(fù)制代碼

在設(shè)置調(diào)用函數(shù)時,asyncAfter有兩個及其相同的方法,不同的地方在于參數(shù)名有所不同,參照Stack Overflow的解釋。

wallDeadline 和 deadline,當系統(tǒng)睡眠后,wallDeadline會繼續(xù),但是deadline會被掛起。例如:設(shè)置參數(shù)為60分鐘,當系統(tǒng)睡眠50分鐘,wallDeadline會在系統(tǒng)醒來之后10分鐘執(zhí)行,而deadline會在系統(tǒng)醒來之后60分鐘執(zhí)行。

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")
}
復(fù)制代碼

DispatchGroup

如果想等到所有的隊列的任務(wù)執(zhí)行完畢再進行某些操作時,可以使用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內(nèi)所有線程的任務(wù)執(zhí)行完畢
group.notify(queue: DispatchQueue.main) {
    print("done")
}

output: 
5
0
6
1
7
2
8
3
9
4
done
復(fù)制代碼

如果想等待某一隊列先執(zhí)行完畢再執(zhí)行其他隊列可以使用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內(nèi)所有線程的任務(wù)執(zhí)行完畢
group.notify(queue: DispatchQueue.main) {
    print("done")
}
output:
0
1
2
3
4
5
6
7
8
9
done
復(fù)制代碼

為防止隊列執(zhí)行任務(wù)時出現(xiàn)阻塞,導(dǎo)致線程鎖死,可以設(shè)置超時時間。

group.wait(timeout: <#T##DispatchTime#>)
group.wait(wallTimeout: <#T##DispatchWallTime#>)
復(fù)制代碼

DispatchWorkItem

Swift3新增的api,可以通過此api設(shè)置隊列執(zhí)行的任務(wù)。先看看簡單應(yīng)用吧。通過DispatchWorkItem初始化閉包。

let workItem = DispatchWorkItem {
    for i in 0..<10 {
        print(i)
    }
}
復(fù)制代碼

調(diào)用一共分兩種情況,第一種是通過調(diào)用perform(),自動響應(yīng)閉包。

 DispatchQueue.global().async {
     workItem.perform()
 }
復(fù)制代碼

第二種是作為參數(shù)傳給async方法。

 DispatchQueue.global().async(execute: workItem)
復(fù)制代碼

接下來我們來看看DispatchWorkItem的內(nèi)部都有些什么方法和屬性。

init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
    block: @escaping () -> Void)
復(fù)制代碼

從初始化方法開始,DispatchWorkItem也可以設(shè)置優(yōu)先級,另外還有個參數(shù)DispatchWorkItemFlags,來看看DispatchWorkItemFlags的內(nèi)部組成。

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
}
復(fù)制代碼

DispatchWorkItemFlags主要分為兩部分:

  • 覆蓋
    • noQoS 沒有優(yōu)先級
    • inheritQoS 繼承Queue的優(yōu)先級
    • enforceQoS 覆蓋Queue的優(yōu)先級
  • 執(zhí)行情況
    • barrier
    • detached
    • assignCurrentContext

執(zhí)行情況會在下文會具體描述,先在這留個坑。
先來看看設(shè)置優(yōu)先級,會對任務(wù)執(zhí)行有什么影響。

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
復(fù)制代碼

由結(jié)果可見即使設(shè)置了DispatchWorkItem僅僅只設(shè)置了優(yōu)先級并不會對任務(wù)執(zhí)行順序有任何影響。
接下來,再來設(shè)置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
復(fù)制代碼

設(shè)置enforceQoS,使優(yōu)先級強制覆蓋queue的優(yōu)先級,所以兩個隊列呈交替執(zhí)行狀態(tài),變?yōu)橥粌?yōu)先級。

DispatchWorkItem也有waitnotify方法,和DispatchGroup用法相同。

DispatchSemaphore

如果你想同步執(zhí)行一個異步隊列任務(wù),可以使用信號量。
wait()會使信號量減一,如果信號量大于1則會返回.success,否則返回timeout(超時),也可以設(shè)置超時時間。

func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult
復(fù)制代碼

signal()會使信號量加一,返回當前信號量。

func signal() -> Int
復(fù)制代碼

下面通過實例來看看具體的使用。
先看看不使用信號量時,在文件異步寫入會發(fā)生什么。

//初始化信號量為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)

//循環(huán)寫入,預(yù)期結(jié)果為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()
        }
    }
}
復(fù)制代碼

<figure>[圖片上傳中...(image-135c2a-1545206459030-1)]

<figcaption></figcaption>

</figure>

發(fā)現(xiàn)寫入的結(jié)果根本不是我們想要的。此時再使用信號量試試。

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()
        }
    }
}
復(fù)制代碼

<figure>[圖片上傳中...(image-f59274-1545206459030-0)]

<figcaption></figcaption>

</figure>

寫入的結(jié)果符合預(yù)期效果,
我們來看下for循環(huán)里都發(fā)生了什么。第一遍循環(huán)遇到wait時,此時信號量為1,大于0,所以if判斷為true,進行寫入操作;當?shù)诙檠h(huán)遇到wait時,發(fā)現(xiàn)信號量為0,此時就會鎖死線程,直到上一遍循環(huán)的寫入操作完成,調(diào)用signal()方法,信號量加一,才會執(zhí)行寫入操作,循環(huán)以上操作。好奇的同學(xué),可以加上sleep(1),然后打開文件夾,會發(fā)現(xiàn)test.txt文件從test1不斷加1變?yōu)?code>test4。(ps:寫入文件的方式略顯粗糙,不過這不是本文討論的重點,僅用以測試DispatchSemaphore)

DispatchSemaphore還有另外一個用法,可以限制隊列的最大并發(fā)量,通過前面所說的wait()信號量減一,signal()信號量加一,來完成此操作,正如上文所述例子,其實達到的效果就是最大并發(fā)量為一。
如果使用過NSOperationQueue的同學(xué),應(yīng)該知道maxConcurrentOperationCount,效果是類似的。

DispatchWorkItemFlags

前面留了個DispatchWorkItemFlags的坑,現(xiàn)在來具體看看。

barrier

可以理解為隔離,還是以文件讀寫為例,在讀取文件時,可以異步訪問,但是如果突然出現(xiàn)了異步寫入操作,我們想要達到的效果是在進行寫入操作的時候,使讀取操作暫停,直到寫入操作結(jié)束,再繼續(xù)進行讀取操作,以保證讀取操作獲取的是文件的最新內(nèi)容。
以上文中的test.txt文件為例,預(yù)期結(jié)果是:在寫入操作之前,讀取到的內(nèi)容是test4;在寫入操作之后,讀取到的內(nèi)容是done(即寫入的內(nèi)容)。
先看看不使用barrier的結(jié)果。

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
復(fù)制代碼

結(jié)果不是我們想要的。再來看看加了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
復(fù)制代碼

結(jié)果符合預(yù)期的想法,barrier主要用于讀寫隔離,以保證寫入的時候,不被讀取。

作者:FFIB
鏈接:https://juejin.im/post/5a4c542b6fb9a045211f17ac
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容