iOS 多執行緒記錄(二)

即將成為型男的濤發表於2019-04-16

前言

iOS 多執行緒記錄(二)

關於這篇文章的Demo可以去我的github中MultiThreadDemo檢視原始碼,如有不當之處,希望大家指出。

這裡是我的上一篇關於多執行緒的知識點記錄 iOS 多執行緒記錄(一)

GCD

介紹

GCD是蘋果開發的多執行緒程式設計的解決方案,通過簡單的API就可以實現建立新執行緒去執行我們需要執行的任務,不需要我們手動地建立和管理執行緒,只需要建立佇列和相應的函式配合使用就行。

優點

  • GCD 可用於多核的並行運算
  • GCD 會自動利用更多的 CPU 核心(比如雙核、四核)
  • GCD 會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)
  • 程式設計師只需要告訴 GCD 想要執行什麼任務,不需要編寫任何執行緒管理程式碼

核心概念

  • 佇列

這裡的佇列指執行任務的等待佇列,即用來存放任務的佇列。佇列是一種特殊的線性表,採用 FIFO(先進先出)的原則,即新任務總是被插入到佇列的末尾,而讀取任務的時候總是從佇列的頭部開始讀取。每讀取一個任務,則從佇列中釋放一個任務。

在 GCD 中有兩種佇列:序列佇列和併發佇列。兩者的主要區別是:執行順序不同,以及開啟執行緒數不同

  • 序列佇列

    • 每次只有一個任務被執行。讓任務一個接著一個地執行。(只開啟一個執行緒,一個任務執行完畢後,再執行下一個任務)
  • 併發佇列

    • 可以讓多個任務併發執行。(可以開啟多個執行緒,並且同時執行任務)

併發佇列 的併發功能只有在非同步函式下才有效。

  • 任務

就是我們需要執行的操作。執行任務有兩種方式:同步執行(sync)和非同步執行(async)。兩者的主要區別是:是否等待佇列的任務執行結束,以及是否具備開啟新執行緒的能力

  • 同步執行(sync)
    • 同步新增任務到指定的佇列中,會在前面的任務執行完成之後,再執行。
    • 只能在當前執行緒中執行任務,不具備開啟新執行緒的能力。
  • 非同步執行(async)
    • 非同步新增任務到指定的佇列中,它不會做任何等待,可以直接執行任務。
    • 可以在新的執行緒中執行任務,具備開啟新執行緒的能力。

基本使用

  1. 建立一個佇列
  2. 在佇列中新增任務

建立佇列

  • 獲取主佇列
// 獲取主佇列
let mainQueue = DispatchQueue.main
複製程式碼
  • 獲取全域性佇列
// 獲取全域性佇列
let globalQueue = DispatchQueue.global()
複製程式碼
  • 建立佇列
  1. 簡單方式建立

指定佇列的名稱,其它為預設項,這樣的初始化的列隊有著預設的配置項,預設的列隊是序列列隊

let queue = DispatchQueue(label: "com.jiangT.queue")
複製程式碼
  1. 屬性設定方式建立
let queue = DispatchQueue.init(label: "com.jiangT.queue",
                                qos: DispatchQoS.default,
                                attributes:DispatchQueue.Attributes.concurrent,
                                autoreleaseFrequency:DispatchQueue.AutoreleaseFrequency.inherit,
                                target: nil)
複製程式碼

引數介紹:

  • label: 佇列的識別符號,能夠方便區分列隊進行除錯。
  • qos: 佇列的優先順序。

優先順序由最低的background到最高的userInteractive共五個,還有一個為定義的unspecified.

background:最低優先順序,等同於DISPATCH_QUEUE_PRIORITY_BACKGROUND.使用者不可見,比如:在後臺儲存大量資料

utility:優先順序等同於DISPATCH_QUEUE_PRIORITY_LOW,可以執行很長時間,再通知使用者結果。比如:下載一個大檔案,網路,計算

default:預設優先順序,優先順序等同於DISPATCH_QUEUE_PRIORITY_DEFAULT,建議大多數情況下使用預設優先順序

userInitiated:優先順序等同於DISPATCH_QUEUE_PRIORITY_HIGH,需要立刻的結果

.userInteractive:使用者互動相關,為了好的使用者體驗,任務需要立馬執行。使用該優先順序用於UI更新,事件處理和小工作量任務,在主執行緒執行。

Qos指定了列隊工作的優先順序,系統會根據優先順序來排程工作,越高的優先順序能夠越快被執行,但是也會消耗功能,所以準確的指定優先順序能夠保證app有效的使用資源。

  • attributes: 佇列的屬性,可以指定併發還是序列。
  • autoreleaseFrequency: 自動釋放頻率,有些列隊會在執行完任務之後自動釋放,有些是不會自動釋放的,需要手動釋放。

新增任務

  • 同步任務
let globalQueue = DispatchQueue.global()

globalQueue.sync {
    print("sync + \(Thread.current)")
}
複製程式碼
  • 非同步任務
let globalQueue = DispatchQueue.global()

globalQueue.async {
    print("async + \(Thread.current)")
}
複製程式碼

雖然使用 GCD 只需兩步,但是既然我們有兩種佇列(序列佇列/併發佇列),兩種任務執行方式(同步執行/非同步執行),那麼我們就有了四種不同的組合方式。再加上兩種特殊佇列:全域性併發佇列、主佇列。全域性併發佇列可以作為普通併發佇列來使用。但是主佇列因為有點特殊,所以我們就又多了兩種組合方式。這樣就有六種不同的組合方式了。

  1. 併發佇列 + 同步執行
  2. 併發佇列 + 非同步執行
  3. 序列佇列 + 同步執行
  4. 序列佇列 + 非同步執行
  5. 主佇列 + 同步執行
  6. 主佇列 + 非同步執行

各種組合的結果

區別 併發佇列 序列佇列 主佇列
同步(sync) 沒有開啟新執行緒,序列執行任務 沒有開啟新執行緒,序列執行任務 會造成死鎖
非同步(async) 有開啟新執行緒,併發執行任務 有開啟新執行緒(1條),序列執行任務 沒有開啟新執行緒,序列執行任務

併發佇列 + 同步執行

DispatchQueue.global().sync {
    print("sync1 + \(Thread.current)")
}
DispatchQueue.global().sync {
    print("sync2 + \(Thread.current)")
}

-----輸出結果:-----
sync1 + <NSThread: 0x600003bc35c0>{number = 1, name = main}
sync2 + <NSThread: 0x600003bc35c0>{number = 1, name = main}
複製程式碼

結論:

  • 所有任務都是在當前執行緒(主執行緒)中執行的,沒有開啟新的執行緒(同步執行不具備開啟新執行緒的能力)。
  • 任務按順序執行的。按順序執行的原因:雖然併發佇列可以開啟多個執行緒,並且同時執行多個任務。但是同步任務不具備開啟新執行緒的能力,所以也就不存在併發。而且當前執行緒只有等待當前佇列中正在執行的任務執行完畢之後,才能繼續接著執行下面的操作(同步任務需要等待佇列的任務執行結束)。所以任務只能一個接一個按順序執行,不能同時被執行。

併發佇列 + 非同步執行

DispatchQueue.global().async {
    print("begin")
    print("async1 + \(Thread.current)")
}
DispatchQueue.global().async {
    print("begin")
    print("async2 + \(Thread.current)")
}
DispatchQueue.global().async {
    print("begin")
    print("async3 + \(Thread.current)")
}
DispatchQueue.global().async {
    print("begin")
    print("async4 + \(Thread.current)")
}
DispatchQueue.global().async {
    print("begin")
    print("async5 + \(Thread.current)")
}

-----輸出結果:-----
begin
begin
begin
async1 + <NSThread: 0x60000018e540>{number = 3, name = (null)}
begin
async2 + <NSThread: 0x600000196600>{number = 4, name = (null)}
begin
async3 + <NSThread: 0x6000001bc900>{number = 5, name = (null)}
async5 + <NSThread: 0x600000196600>{number = 4, name = (null)}
async4 + <NSThread: 0x60000018e540>{number = 3, name = (null)}
複製程式碼

結論:

  • 除了當前執行緒(主執行緒),系統又開啟了3個執行緒,並且任務是交替/同時執行的。(非同步執行具備開啟新執行緒的能力。且併發佇列可開啟多個執行緒,同時執行多個任務)。

序列佇列 + 同步執行

let queue = DispatchQueue.init(label: "")

queue.sync {
    print("sync1 + \(Thread.current)")
}
queue.sync {
    print("sync1 + \(Thread.current)")
}

-----輸出結果:-----
sync1 + <NSThread: 0x600000a968c0>{number = 1, name = main}
sync2 + <NSThread: 0x600000a968c0>{number = 1, name = main}

複製程式碼

結論:

  • 所有任務都是在當前執行緒(主執行緒)中執行的,並沒有開啟新的執行緒(同步執行不具備開啟新執行緒的能力)。
  • 任務是按順序執行的(序列佇列每次只有一個任務被執行,任務一個接一個按順序執行)。

序列佇列 + 非同步執行

let queue = DispatchQueue.init(label: "")

queue.async {
    print("begin")
    print("async1 + \(Thread.current)")
}
queue.async {
    print("begin")
    print("async2 + \(Thread.current)")
}
queue.async {
    print("begin")
    print("async3 + \(Thread.current)")
}

-----輸出結果:-----
begin
async1 + <NSThread: 0x60000032b940>{number = 3, name = (null)}
begin
async2 + <NSThread: 0x60000032b940>{number = 3, name = (null)}
begin
async3 + <NSThread: 0x60000032b940>{number = 3, name = (null)}
複製程式碼

結論:

  • 開啟了一條新執行緒(非同步執行具備開啟新執行緒的能力,序列佇列只開啟一個執行緒)。
  • 任務是按順序執行的(序列佇列每次只有一個任務被執行,任務一個接一個按順序執行)。

主佇列 + 同步執行

  • 主佇列:GCD自帶的一種特殊的序列佇列
// 主執行緒執行同步任務
func syncMain() {
    print("方法開始")   // 呼叫方法後,可以看到這個輸出
    DispatchQueue.main.sync {
        print("會造成死鎖")
    }
}

-----輸出結果:-----
方法開始
複製程式碼

結論:

  1. 當把任務放進主佇列時,它需要等待主佇列執行完當前任務後執行。
  2. 主執行緒現在正在處理 syncMain 方法,任務需要等 syncMain 執行完才能執行。
  3. syncMain 在執行時,又要等任務執行完才能完成方法。
  4. 這樣 syncMain 方法和任務就開始了互相等待,形成了死鎖。

主佇列 + 非同步執行

DispatchQueue.main.async {
    print("begin")
    print("async1 + \(Thread.current)")
}
DispatchQueue.main.async {
    print("begin")
    print("async2 + \(Thread.current)")
}
DispatchQueue.main.async {
    print("begin")
    print("async3 + \(Thread.current)")
}
DispatchQueue.main.async {
    print("begin")
    print("async4 + \(Thread.current)")
}
DispatchQueue.main.async {
    print("begin")
    print("async5 + \(Thread.current)")
}

-----輸出結果:-----
begin
async1 + <NSThread: 0x600001af6880>{number = 1, name = main}
begin
async2 + <NSThread: 0x600001af6880>{number = 1, name = main}
begin
async3 + <NSThread: 0x600001af6880>{number = 1, name = main}
begin
async4 + <NSThread: 0x600001af6880>{number = 1, name = main}
begin
async5 + <NSThread: 0x600001af6880>{number = 1, name = main}
複製程式碼

結論:

  • 所有任務都是在當前執行緒(主執行緒)中執行的,並沒有開啟新的執行緒(雖然非同步執行具備開啟執行緒的能力,但因為是主佇列,所以所有任務都在主執行緒中)。
  • 任務是按順序執行的(因為主佇列是序列佇列,每次只有一個任務被執行,任務一個接一個按順序執行)。

執行緒間通訊

/**
 * 執行緒間通訊
 */
DispatchQueue.global().async {
    print("async-begin")
    print("global + \(Thread.current)")
    print("async-end")
    
    let testNum = 666
    
    // 回到主執行緒
    DispatchQueue.main.async {
        print("main + \(Thread.current)")
        print("pass testNum: \(testNum)")
    }
}
-----輸出結果:-----
async-begin
global + <NSThread: 0x60000383d100>{number = 3, name = (null)}
async-end
main + <NSThread: 0x600003821680>{number = 1, name = main}
pass testNum: 666
複製程式碼

結論:

  • 可以看到在全域性執行緒中先執行任務,執行完了之後回到主執行緒執行主執行緒的相應操作並將全域性執行緒的testNum接收到。

DispatchWorkItem

DispatchWorkItem是用於幫助DispatchQueue來執行列隊中的任務。

一般情況下,我們開啟一個非同步執行緒,會這樣建立列隊並執行async方法,以閉包的方式提交任務。

DispatchQueue.global().async {
    // do async task
}
複製程式碼

使用了DispatchWorkItem類將任務封裝成為物件,由物件進行任務。

let item = DispatchWorkItem {
      // do task
 }
 DispatchQueue.global().async(execute: item)
複製程式碼

也可以使用DispatchWorkItem例項物件的perform方法執行任務

let workItem = DispatchWorkItem {
     // do task
 }
 DispatchQueue.global().async {
    workItem.perform()
 }
複製程式碼
柵欄方法

我們有時需要非同步執行兩組操作,而且第一組操作執行完之後,才能開始執行第二組操作。這樣我們就需要一個相當於 柵欄 一樣的一個方法將兩組非同步執行的操作組給分割起來,當然這裡的操作組裡可以包含一個或多個任務。

柵欄 會等待前邊追加到併發佇列中的任務全部執行完畢之後,再將指定的任務追加到該非同步佇列中。

let dataQueue = DispatchQueue(label: "com.jiangT.queue", attributes: .concurrent)

let item = DispatchWorkItem(qos: .default, flags: .barrier) {
    print("barrier + \(Thread.current)")
    dataQueue.async {
        print("async1 + \(Thread.current)")
    }
    dataQueue.async {
        print("async2 + \(Thread.current)")
    }
}

dataQueue.async(execute: item)
dataQueue.async {
    print("async3 + \(Thread.current)")
}
dataQueue.async {
    print("async4 + \(Thread.current)")
}

-----輸出結果:-----
barrier + <NSThread: 0x600000f9d800>{number = 3, name = (null)}
async3 + <NSThread: 0x600000f9da40>{number = 6, name = (null)}
async4 + <NSThread: 0x600000f9bac0>{number = 7, name = (null)}
async1 + <NSThread: 0x600000f9d800>{number = 3, name = (null)}
async2 + <NSThread: 0x600000fa4040>{number = 8, name = (null)}
複製程式碼
執行任務結束通過nofify獲得通知
let workItem = DispatchWorkItem {
    // do async task
    print(Thread.current)
}
DispatchQueue.global().async {
    workItem.perform()
}
workItem.notify(queue: DispatchQueue.main) {
    // update UI
    print(Thread.current)
}

-----輸出結果:-----
<NSThread: 0x60000193ce40>{number = 3, name = (null)}
<NSThread: 0x600001921100>{number = 1, name = main}
複製程式碼
使用wait等待任務執行完成
let queue = DispatchQueue(label: "queue", attributes: .concurrent)
let workItem = DispatchWorkItem {
    sleep(5)
    print("done")
}

queue.async(execute: workItem)
print("before waiting")
workItem.wait()
print("after waiting")

-----輸出結果:-----
before waiting
done
after waiting
複製程式碼

延遲執行方法

let delay = DispatchTime.now() + DispatchTimeInterval.seconds(10)
 DispatchQueue.main.asyncAfter(deadline: delay) {
     // 延遲執行
 }
 
 可以簡化為:
 
 let delay = DispatchTime.now() + 10
 DispatchQueue.main.asyncAfter(deadline: delay) {
     // 延遲執行
 }
複製程式碼

其它的延時執行方法:

func asyncAfter(deadline: DispatchTime, execute: DispatchWorkItem)
func asyncAfter(deadline: DispatchTime, qos: DispatchQoS, flags: DispatchWorkItemFlags, execute: () -> Void)

func asyncAfter(wallDeadline: DispatchWallTime, execute: DispatchWorkItem)
func asyncAfter(wallDeadline: DispatchWallTime, qos: DispatchQoS, flags: DispatchWorkItemFlags, execute: () -> Void)
複製程式碼

快速迭代方法

之前使用GCD的dispatch_apply()執行多次任務,現在是呼叫concurrentPerform(),下面是併發執行5次

DispatchQueue.concurrentPerform(iterations: 5) {
    print("\($0)")
}

-----輸出結果:-----
2
0
1
3
4
複製程式碼

佇列組操作

有時候我們會有這樣的需求:分別非同步執行2個耗時任務,然後當2個耗時任務都執行完畢後再回到主執行緒執行任務。這時候我們可以用到 GCD 的佇列組。

let queue = DispatchQueue.global()
let group = DispatchGroup()

group.enter()
queue.async(group: group) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
        print("Task one finished")
        group.leave()
    })
}
group.enter()
queue.async(group: group) {
    print("Task two finished")
    group.leave()
}
group.enter()
queue.async(group: group) {
    print("Task three finished")
    group.leave()
}
group.notify(queue: queue) {
    print("All task has finished")
}

----輸出結果:-----
Task two finished
Task three finished
Task one finished
All task has finished

複製程式碼

相關文章