iOS Swift GCD 開發教程



presented by sinking soul

本教程將帶你詳細瞭解 GCD 的概念和用法,經過文中的代碼示例和附帶的 Github 示例工程,能夠進一步加深對這些概念的體會。附帶的示例工程是一個完整可運行的 App 項目:DispatchQueueTest,項目地址點此處。本教程提供離線版,閱讀體驗更佳: HTML 版PDF 版html

GCD 全稱是 Grand Central Dispatch,翻譯過來就是大規模中央調度。根據官方文檔,它的做用是:「經過向系統管理的調度隊列中提交任務,在多核硬件上同時執行代碼。」。它提供了一套機制,讓你能夠充分利用硬件的多核性能,而且讓你不用再調用那些繁瑣的底層線程 API,編寫易於理解和修改的代碼。

node

1. 隊列和任務的概念

GCD 的核心就是爲了解決如何讓程序有序、高效的運行,由此衍生出隊列等概念和一系列的方法。爲了弄清楚這些概念,咱們先來看看程序執行存在哪些問題須要解決。git

理解任務

在 GCD 中把程序執行時作的事情都當成任務,一段代碼、一個 API 調用、一個方法、函數、閉包等,都是任務,一個應用就是由不少任務組成的。任務的執行須要時間和相應的順序,耗時有長短,順序有前後,任務只有按照正確的時間和順序進行編排,應用才能按照你的預期運行。咱們舉音樂播放的例子來看看關於任務有哪些需求。github

  1. 默認狀況下,程序是按代碼順序執行的,但咱們有時但願應用能同時作多件事情,好比同時下載歌詞和音樂。這就有了第一個需求:讓多個任務同時進行。
  2. 對於下載這個任務,能夠一次下載多首音樂,各下各的,不須要互相等待;然而當所有下載完了播放時,一般是一首接一首的播放,播放一首音樂這個任務是須要等待前面的播聽任務完成了才能進行。這就有了第二個需求:有的任務須要等待它完成了才能進行下一個任務,有的任務不須要等待它完成。
  3. 若是一首音樂還沒下載,咱們就點了播放鍵,咱們看看須要作哪些事情:它須要把歌詞、音樂分別下載了,等他們都下載完了,告訴應用你能夠播放了,而後應用把歌詞、音樂同時播放。那咱們怎麼知道歌詞、音樂都下載完了呢?這就有了第三個需求:若是有個東西能把幾個任務捆綁到一塊兒就行了,當整個包都完成了再通知我。
  4. 仍是下載,若是咱們勾選了一堆的音樂要下載,中間我想暫停一下,過一會再讓它繼續,這就要求這一系列的下載任務要能夠暫停和繼續。
  5. 通常下載工具均可以設置同時最大下載數,這就要求有一個方法能夠控制同時進行的任務數。
  6. 不少播放器會有一個功能:播放 20 分鐘後就中止,很是適合睡覺前用。這個時候須要有個任務,在 20 分鐘後把音樂關了。延遲執行任務就是它須要的特性。

以上列舉了 6 個經典的任務執行須要的特性,在 GCD 中分別提供瞭如下方法來支持它們:macos

  1. 串行隊列、並行隊列
  2. 同步任務、異步任務
  3. 任務組、柵欄任務
  4. 掛起、喚醒隊列
  5. 信號量
  6. 延遲加入隊列

下面咱們先從隊列開始分析。編程

2. 建立隊列

在系統底層,程序是運行在線程之中的,若是咱們直接在線程層面進行操做,咱們就須要告訴程序它應該運行在哪一個線程、什麼時候開始、什麼時候結束等,這一列的操做都很是繁瑣,並且很容易出錯。爲了簡化線程的操做,GCD 封裝了隊列的概念。swift

能夠把隊列想象成辦事窗口,有些類型窗口一次只能受理一個任務,一般只有一個辦事員(線程),全部任務按進入的前後順序來辦理,並且不容許插隊(阻塞線程),這是串行隊列。api

有些類型窗口一次能夠受理多個任務,多個任務能夠同時辦理,一般有多個辦事員(線程),並且同一個任務在辦理過程當中容許被插隊(阻塞線程),這是並行隊列。數組

在後面咱們會詳細討論隊列的特性。安全

建立隊列很是的簡單。

串行隊列

系統爲串行隊列通常只分配一個線程(也有特例,下一章任務特性部分有解釋),隊列中若是有任務正在執行時,是不容許隊列中的其餘任務插隊的(即暫停當前任務,轉而執行其餘任務),這個特性也能夠理解爲:串行隊列中執行任務的線程不容許被當前隊列中的任務阻塞(此時會死鎖),但能夠被別的隊列任務阻塞。

建立時指定 label 便於調試,通常使用 Bundle Identifier 相似的命名方式:

let queue = DispatchQueue(label: "com.xxx.xxx.queueName")
複製代碼

並行隊列

系統會爲並行隊列至少分配一個線程,線程容許被任何隊列的任務阻塞。

let queue = DispatchQueue(label: "com.xxx.xxx.queueName", attributes: .concurrent)
複製代碼

其實在咱們手動建立隊列以前,系統已經幫咱們建立好了 6 條隊列,1 條系統主隊列(串行),5 條全局併發隊列(不一樣優先級),它們是咱們建立的全部隊列的最終目標隊列(後面會解釋),這 6 個隊列負責全部隊列的線程調度。

系統主隊列

主隊列是一個串行隊列,它主要處理 UI 相關任務,也能夠處理其餘類型任務,但爲了性能考慮,儘可能讓主隊列執行 UI 相關或少許不耗時間和資源的操做。它經過類屬性獲取:

let mainQueue = DispatchQueue.main
複製代碼

系統全局併發隊列

全局併發隊列,存在 5 個不一樣的 QoS 級別,可使用默認優先級,也能夠單獨指定:

let globalQueue = DispatchQueue.global() // qos: .default
let globalQueue = DispatchQueue.global(qos: .background) // 後臺運行級別
複製代碼

3. 添加隊列任務

有些任務咱們必須等待它的執行結果才能進行下一步,這種執行任務的方式稱爲同步,簡稱同步任務;有些任務只要把它放入隊列就能夠無論它了,能夠繼續執行其餘任務,按這種方式執行的任務,稱爲異步任務。

同步任務

特性:任務一經提交就會阻塞當前線程(當前線程能夠理解爲下方代碼示例中執行 sync 方法所在的線程 thread0),並請求隊列當即安排其執行,執行任務的線程 thread1 默認等於 thread0,即同步任務直接在當前線程運行,任務完成後恢復線程原任務。

任務提交方式以下:

// current thread - thread0
queue.sync {
    // current thread - thread1 == thread0
    // do something
}
複製代碼

咱們分別根據下圖中的 4 種狀況舉 4 個例子,來講明同步任務的特性。

  1. 隊列中若是沒有任務在執行,那麼提交同步任務後,將當即執行該任務,並阻塞線程 thread0,任務完成後再恢復線程 thread0 中被阻塞的任務。
  2. 若是串行隊列中有任務在執行,若是該任務又向該隊列提交了一個同步任務,將會當即發生死鎖。
  3. 若是並行隊列中有任務在執行,若是該任務又向該隊列提交了一個同步任務,那麼當前線程會轉而執行新的同步任務,結束後再回到原任務。
  4. 若是隊列中有任務在執行,若是該任務向另外一個隊列提交了一個同步任務,那麼當前線程會轉而執行新的同步任務,結束後再回到原任務。

看例子前先介紹兩個輔助方法:

1.打印當前線程,使用 Thread.current 屬性:

/// 打印當前線程
func printCurrentThread(with des: String, _ terminator: String = "") {
    print("\(des) at thread: \(Thread.current), this is \(Thread.isMainThread ? "" : "not ")main thread\(terminator)")
}
複製代碼

2.測試任務是否在指定隊列中,經過給隊列設置一個標識,使用 DispatchQueue.getSpecific 方法來獲取這個標識,若是能獲取到,說明任務在該隊列中:

/// 隊列類型
enum DispatchTaskType: String {
    case serial
    case concurrent
    case main
    case global
}

// 定義隊列
let serialQueue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue")
let concurrentQueue = DispatchQueue(
    label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue",
    attributes: .concurrent)
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()

// 定義隊列 key
let serialQueueKey = DispatchSpecificKey<String>()
let concurrentQueueKey = DispatchSpecificKey<String>()
let mainQueueKey = DispatchSpecificKey<String>()
let globalQueueKey = DispatchSpecificKey<String>()

// 初始化隊列 key
init() {
    serialQueue.setSpecific(key: serialQueueKey, value: DispatchTaskType.serial.rawValue)
    concurrentQueue.setSpecific(key: concurrentQueueKey, value: DispatchTaskType.concurrent.rawValue)
    mainQueue.setSpecific(key: mainQueueKey, value: DispatchTaskType.main.rawValue)
    globalQueue.setSpecific(key: globalQueueKey, value: DispatchTaskType.global.rawValue)
}

/// 測試任務是否在指定隊列中
func testIsTaskInQueue(_ queueType: DispatchTaskType, key: DispatchSpecificKey<String>) {
    let value = DispatchQueue.getSpecific(key: key)
    let opnValue: String? = queueType.rawValue
    print("Is task in \(queueType.rawValue) queue: \(value == opnValue)")
}
複製代碼

下面咱們看看這 4 個例子:

代碼示例

本章對應的代碼見示例工程中 QueueTestListTableViewController+createQueueWithTask.swift, CreateQueueWithTask.swift.

示例 3.1:串行隊列中新增同步任務

/// 串行隊列中新增同步任務
func testSyncTaskInSerialQueue() {
    self.printCurrentThread(with: "start test")
    serialQueue.sync {
        print("\nserialQueue sync task--->")
        self.printCurrentThread(with: "serialQueue sync task")
        self.testIsTaskInQueue(.serial, key: serialQueueKey)
        print("--->serialQueue sync task\n")
    }
    self.printCurrentThread(with: "end test")
}
複製代碼

執行結果,任務是在主線程中執行的,結束後又回到了主線程,能夠理解爲這個同步任務把主線程阻塞了,讓本身優先插隊執行:

start test at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread

serialQueue sync task--->
serialQueue sync task at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread
Is task in serial queue: true
--->serialQueue sync task

end test at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread

示例 3.2 串行隊列任務中嵌套本隊列的同步任務

/// 串行隊列任務中嵌套本隊列的同步任務
func testSyncTaskNestedInSameSerialQueue() {
    printCurrentThread(with: "start test")
    serialQueue.async {
        print("\nserialQueue async task--->")
        self.printCurrentThread(with: "serialQueue async task")
        self.testIsTaskInQueue(.serial, key: self.serialQueueKey)

        self.serialQueue.sync {
            print("\nserialQueue sync task--->")
            self.printCurrentThread(with: "serialQueue sync task")
            self.testIsTaskInQueue(.serial, key: self.serialQueueKey)
            print("--->serialQueue sync task\n")
        } // Thread 9: EXC_BREAKPOINT (code=1, subcode=0x101613ba4)

        print("--->serialQueue async task\n")
    }
    printCurrentThread(with: "end test")
}
複製代碼

執行結果,執行到嵌套任務時程序就崩潰了,這是死鎖致使的。其中有個有意思的現象,這裏串行隊列的第一個任務運行在非主線程上,在異步任務部分會解釋。這裏死鎖是由兩個因素致使:串行隊列、同步任務,回顧一下串行隊列的特性就好解釋了:串行隊列中執行任務的線程不容許被當前隊列中的任務阻塞。下個例子咱們試試:並行隊列 + 同步任務,看看會不會致使死鎖。

start test at thread: <NSThread: 0x1c006db80>{number = 1, name = main}, this is main thread

serialQueue async task--->
end test at thread: <NSThread: 0x1c006db80>{number = 1, name = main}, this is main thread
serialQueue async task at thread: <NSThread: 0x1c4466340>{number = 3, name = (null)}, this is not main thread
Is task in serial queue: true

(lldb)

示例 3.3 並行隊列任務中嵌套本隊列的同步任務

/// 並行隊列任務中嵌套本隊列的同步任務
func testSyncTaskNestedInSameConcurrentQueue() {
    printCurrentThread(with: "start test")
    concurrentQueue.async {
        print("\nconcurrentQueue async task--->")
        self.printCurrentThread(with: "concurrentQueue async task")
        self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)

        self.concurrentQueue.sync {
            print("\nconcurrentQueue sync task--->")
            self.printCurrentThread(with: "concurrentQueue sync task")
            self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)
            print("--->concurrentQueue sync task\n")
        }

        print("--->concurrentQueue async task\n")
    }
    printCurrentThread(with: "end test")
}
複製代碼

執行結果,嵌套的同步任務執行的很是順利,並且印證了同步任務的另外一個特性:同步任務直接在當前線程運行。

start test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread

concurrentQueue async task--->
end test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
concurrentQueue async task at thread: <NSThread: 0x1c426cd80>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true

concurrentQueue sync task--->
concurrentQueue sync task at thread: <NSThread: 0x1c426cd80>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true
--->concurrentQueue sync task

--->concurrentQueue async task

示例 3.4:串行隊列中嵌套其餘隊列的同步任務

/// 串行隊列中嵌套其餘隊列的同步任務
func testSyncTaskNestedInOtherSerialQueue() {
    // 創新另外一個串行隊列
    let serialQueue2 = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue2")
    let serialQueueKey2 = DispatchSpecificKey<String>()
    serialQueue2.setSpecific(key: serialQueueKey2, value: "serial2")

    self.printCurrentThread(with: "start test")
    serialQueue.sync {
        print("\nserialQueue sync task--->")
        self.printCurrentThread(with: "nserialQueue sync task")
        self.testIsTaskInQueue(.serial, key: self.serialQueueKey)

        serialQueue2.sync {
            print("\nserialQueue2 sync task--->")
            self.printCurrentThread(with: "serialQueue2 sync task")
            self.testIsTaskInQueue(.serial, key: self.serialQueueKey)

            let value = DispatchQueue.getSpecific(key: serialQueueKey2)
            let opnValue: String? = "serial2"
            print("Is task in serialQueue2: \(value == opnValue)")
            print("--->serialQueue2 sync task\n")
        }

        print("--->serialQueue sync task\n")
    }
}
複製代碼

執行結果,串行隊列嵌套的同步任務執行成功了,和前面的例子不同啊。是的,由於這裏嵌套的是另外一個隊列的任務,雖然它們都運行在同一個線程上,一個串行隊列能夠對另外一個串行隊列視而不見。不一樣隊列複用線程這是系統級的隊列做出的優化,可是在同一個串行隊列內部,任務必定都是按順序執行的,這是自定義隊列的最本質做用。

start test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread

serialQueue sync task--->
nserialQueue sync task at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
Is task in serial queue: true

serialQueue2 sync task--->
serialQueue2 sync task at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
Is task in serial queue: false
Is task in serialQueue2: true
--->serialQueue2 sync task

--->serialQueue sync task

異步任務

特性:任務提交後不會阻塞當前線程,會由隊列安排另外一個線程執行。

任務提交方式以下:

// current thread - thread0
queue.async {
    // current thread - thread1 != thread0
    // do something
}
複製代碼

咱們分別根據下圖舉 3 個例子,來講明異步任務的特性。

下面咱們看看這 3 個例子:

代碼示例

示例3.5:並行隊列中新增異步任務

/// 並行隊列中新增異步任務
func testAsyncTaskInConcurrentQueue() {
    printCurrentThread(with: "start test")
    concurrentQueue.async {
        print("\nconcurrentQueue async task--->")
        self.printCurrentThread(with: "concurrentQueue async task")
        self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)
        print("--->concurrentQueue async task\n")
    }
    printCurrentThread(with: "end test")
}  
複製代碼

執行結果,執行異步任務時新開了一個線程。

start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread

concurrentQueue async task--->
concurrentQueue async task at thread: <NSThread: 0x1c04799c0>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true
--->concurrentQueue async task

示例3.6:串行隊列中新增異步任務

/// 串行隊列中新增異步任務
func testAsyncTaskInSerialQueue() {
    printCurrentThread(with: "start test")
    serialQueue.async {
        print("\nserialQueue async task--->")
        self.printCurrentThread(with: "serialQueue async task")
        self.testIsTaskInQueue(.serial, key: self.serialQueueKey)
        print("--->serialQueue async task\n")
    }
    printCurrentThread(with: "end test")
}
複製代碼

執行結果,一樣新開了一個線程。

start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread

serialQueue async task--->
serialQueue async task at thread: <NSThread: 0x1c4473740>{number = 4, name = (null)}, this is not main thread
Is task in serial queue: true
--->serialQueue async task

示例3.7:串行隊列任務中嵌套本隊列的異步任務

/// 串行隊列任務中嵌套本隊列的異步任務
func testAsyncTaskNestedInSameSerialQueue() {
    printCurrentThread(with: "start test")
    serialQueue.sync {
        print("\nserialQueue sync task--->")
        self.printCurrentThread(with: "serialQueue sync task")
        self.testIsTaskInQueue(.serial, key: self.serialQueueKey)

        self.serialQueue.async {
            print("\nserialQueue async task--->")
            self.printCurrentThread(with: "serialQueue async task")
            self.testIsTaskInQueue(.serial, key: self.serialQueueKey)
            print("--->serialQueue async task\n")
        }

        print("--->serialQueue sync task\n")
    }
    printCurrentThread(with: "end test")
}
複製代碼

執行結果,這個例子再一次刷新了對串行隊列的認識:串行隊列並非只能運行一個線程。第一層的同步任務運行在主線程上,第二層的異步任務運行在其餘線程上,但它們在時間片上是分開的。這裏再嚴格定義一下:串行隊列同一時間只會運行一個線程,只有碰到異步任務時,纔會使用不一樣於當前的線程,但都是按時間順序執行,只有前一個任務完成了,纔會執行下一個任務。

start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread

serialQueue sync task--->
serialQueue sync task at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
Is task in serial queue: true
--->serialQueue sync task

serialQueue async task--->
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
serialQueue async task at thread: <NSThread: 0x1c4473a40>{number = 5, name = (null)}, this is not main thread
Is task in serial queue: true
--->serialQueue async task

這裏咱們總結一下隊列和任務的特性:

  • 串行隊列同一時間只會使用同一線程、運行同一任務,並嚴格按照任務順序執行。
  • 並行隊列同一時間可使用多個線程、運行多個任務,執行順序不分前後。
  • 同步任務會阻塞當前線程,並在當前線程執行。
  • 異步任務不會阻塞當前線程,並在與當前線程不一樣的線程執行。
  • 如何避免死鎖:不要在串行或主隊列中嵌套執行同步任務。

下面介紹兩個特殊的任務類型:柵欄任務、迭代任務。

柵欄任務

柵欄任務的主要特性是能夠對隊列中的任務進行阻隔,執行柵欄任務時,它會先等待隊列中已有的任務所有執行完成,而後它再執行,在它以後加入的任務也必須等柵欄任務執行完後才能執行。

這個特性更適合並行隊列,並且對柵欄任務使用同步或異步方法效果都相同。

  • 建立方式,先建立 WorkItem,標記爲:barrier,再添加至隊列中:
let queue = DispatchQueue(label: "queueName", attributes: .concurrent)
let task = DispatchWorkItem(flags: .barrier) {
    // do something
}
queue.async(execute: task)
queue.sync(execute: task) // 與 async 效果同樣
複製代碼

下面看看柵欄任務的例子:

代碼示例

示例3.8:並行隊列中執行柵欄任務

/// 柵欄任務
func barrierTask() {
    let queue = concurrentQueue
    let barrierTask = DispatchWorkItem(flags: .barrier) {
        print("\nbarrierTask--->")
        self.printCurrentThread(with: "barrierTask")
        print("--->barrierTask\n")
    }

    printCurrentThread(with: "start test")

    queue.async {
        print("\nasync task1--->")
        self.printCurrentThread(with: "async task1")
        print("--->async task1\n")
    }
    queue.async {
        print("\nasync task2--->")
        self.printCurrentThread(with: "async task2")
        print("--->async task2\n")
    }
    queue.async {
        print("\nasync task3--->")
        self.printCurrentThread(with: "async task3")
        print("--->async task3\n")
    }

    queue.async(execute: barrierTask) // 柵欄任務

    queue.async {
        print("\nasync task4--->")
        self.printCurrentThread(with: "async task4")
        print("--->async task4\n")
    }
    queue.async {
        print("\nasync task5--->")
        self.printCurrentThread(with: "async task5")
        print("--->async task5\n")
    }
    queue.async {
        print("\nasync task6--->")
        self.printCurrentThread(with: "async task6")
        print("--->async task6\n")
    }
    printCurrentThread(with: "end test")
}
複製代碼

執行結果,任務 一、二、3 都在柵欄任務前同時執行,任務 四、五、6 都在柵欄任務後同時執行:

start test at thread: <NSThread: 0x1c407e7c0>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c407e7c0>{number = 1, name = main}, this is main thread

async task1--->

async task2--->
async task2 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task2

async task3--->
async task3 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task3

async task1 at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->async task1

barrierTask--->
barrierTask at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->barrierTask

async task5--->
async task5 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task5

async task6--->
async task6 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task6

async task4--->
async task4 at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->async task4

迭代任務

並行隊列利用多個線程執行任務,能夠提升程序執行的效率。而迭代任務能夠更高效地利用多核性能,它能夠利用 CPU 當前全部可用線程進行計算(任務小也可能只用一個線程)。若是一個任務能夠分解爲多個類似但獨立的子任務,那麼迭代任務是提升性能最適合的選擇。

使用 concurrentPerform 方法執行迭代任務,迭代任務的後續任務須要等待它執行完成纔會繼續。本方法相似於 Objc 中的 dispatch_apply 方法,建立方式以下:

DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 爲迭代次數,可修改。
    // do something
}
複製代碼

迭代任務能夠單獨執行,也能夠放在指定的隊列中:

let queue = DispatchQueue.global() // 全局併發隊列
queue.async {
    DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in
        // do something
    }
    //能夠轉至主線程執行其餘任務
    DispatchQueue.main.async {
        // do something
    }
}
複製代碼

下面看看迭代任務的例子:

代碼示例

示例3.9:迭代任務

本示例查找 1-10000 之間能被 13 整除的整數,咱們直接使用 10000 次迭代對每一個數進行判斷,符合的經過異步方法寫入到結果數組中:

/// 迭代任務
func concurrentPerformTask() {
    printCurrentThread(with: "start test")

    /// 判斷一個數是否能被另外一個數整除
    func isDividedExactlyBy(_ divisor: Int, with number: Int) -> Bool {
        return number % divisor == 0
    }

    let array = Array(1...100)
    var result: [Int] = []

    globalQueue.async {
        //經過concurrentPerform,循環變量數組
        print("concurrentPerform task start--->")
        DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in
            if isDividedExactlyBy(13, with: array[index]) {
                self.printCurrentThread(with: "find a match: \(array[index])")
                self.mainQueue.async {
                    result.append(array[index])
                }
            }
        }
        print("--->concurrentPerform task over")
        //執行完畢,主線程更新結果。
        DispatchQueue.main.sync {
            print("back to main thread")
            print("result: find \(result.count) number - \(result)")
        }
    }
    printCurrentThread(with: "end test")
}
複製代碼

iPhone 7 Plus 執行結果,使用了 2 個線程,iPhone 8 的 CPU 有 6 個核心,聽說能夠同時開啓,手頭有的能夠試一下:

start test at thread: <NSThread: 0x1c4076900>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c4076900>{number = 1, name = main}, this is main thread
concurrentPerform task start--->
find a match: 13 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 39 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 52 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 65 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 26 at thread: <NSThread: 0x1c0469cc0>{number = 4, name = (null)}, this is not main thread
find a match: 91 at thread: <NSThread: 0x1c0469cc0>{number = 4, name = (null)}, this is not main thread
find a match: 78 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
--->concurrentPerform task over
back to main thread
result: find 7 number - [13, 39, 52, 65, 26, 91, 78]

Mac 上使用 Xcode 模擬器執行結果,使用了 4 個線程:

start test at thread: <NSThread: 0x604000070c40>{number = 1, name = main}, this is main thread
concurrentPerform task start--->
end test at thread: <NSThread: 0x604000070c40>{number = 1, name = main}, this is main thread
find a match: 26 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 13 at thread: <NSThread: 0x60000046ec80>{number = 4, name = (null)}, this is not main thread
find a match: 65 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 91 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 78 at thread: <NSThread: 0x60000046ec80>{number = 4, name = (null)}, this is not main thread
find a match: 39 at thread: <NSThread: 0x60000046ed80>{number = 5, name = (null)}, this is not main thread
find a match: 52 at thread: <NSThread: 0x604000475140>{number = 6, name = (null)}, this is not main thread
--->concurrentPerform task over
back to main thread
result: find 7 number - [26, 13, 65, 91, 78, 39, 52]

4. 隊列詳細屬性

下面介紹一下在建立隊列時,能夠設置的一些更豐富的屬性。建立隊列的完整方法以下:

convenience init(label: String, qos: DispatchQoS = default, attributes: DispatchQueue.Attributes = default, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = default, target: DispatchQueue? = default)
複製代碼

QoS

隊列在執行上是有優先級的,更高的優先級能夠享受更多的計算資源,從高到低包含如下幾個等級:

  • userInteractive
  • userInitiated
  • default
  • utility
  • background

Attributes

包含兩個屬性:

  • concurrent:標識隊列爲並行隊列
  • initiallyInactive:標識運行隊列中的任務須要動手觸發(未添加此標識時,向隊列中添加任務會自動運行),觸發時經過 queue.activate() 方法。

AutoreleaseFrequency

這個屬性表示 autorelease pool 的自動釋放頻率, autorelease pool 管理着任務對象的內存週期。

包含三個屬性:

  • inherit:繼承目標隊列的該屬性
  • workItem:跟隨每一個任務的執行週期進行自動建立和釋放
  • never:不會自動建立 autorelease pool,須要手動管理。

通常任務採用 .workItem 屬性就夠了,特殊任務如在任務內部大量重複建立對象的操做可選擇 .never 屬性手動建立 autorelease pool

Target

這個屬性設置的是一個隊列的目標隊列,即實際將該隊列的任務放入指定隊列中運行。目標隊列最終約束了隊列優先級等屬性。

在程序中手動建立的隊列,其實最後都指向系統自帶的 主隊列全局併發隊列

你也許會問,爲何不直接將任務添加至系統隊列中,而是自定義隊列,由於這樣的好處是能夠將任務進行分組管理。如單獨阻塞隊列中的任務,而不是阻塞系統隊列中的所有任務。若是阻塞了目標隊列,全部指向它的原隊列也將被阻塞。

在 Swift 3 及以後,對目標隊列的設置進行了約束,只有兩種狀況能夠顯式地設置目標隊列(緣由參考):

  • 初始化方法中,指定目標隊列。
  • 初始化方法中,attributes 設定爲 initiallyInactive,而後在隊列執行 activate() 以前能夠指定目標隊列。

在其餘地方都不能再改變目標隊列。

關於目標隊列的詳細闡述,能夠參考這篇文章:GCD Target Queues

5. 延遲加入隊列

有時候你並不須要當即將任務加入隊列中運行,而是須要等待一段時間後再進入隊列中,這時候可使用 asyncAfter 方法。

例如,咱們封裝一個方法指定延遲加入隊列的時間:

class AsyncAfter {
    /// 延遲執行閉包
    static func dispatch_later(_ time: TimeInterval, block: @escaping ()->()) {
        let t = DispatchTime.now() + time
        DispatchQueue.main.asyncAfter(deadline: t, execute: block)
    }
}

AsyncAfter.dispatch_later(2) {
    print("打個電話 at: \(Date())") // 將在 2 秒後執行
}
複製代碼

這裏要注意延遲的時間是加入隊列的時間,而不是開始執行任務的時間。

下面咱們構造一個複雜一點的例子,咱們封裝一個方法,能夠延遲執行任務,在計時結束前還能夠取消任務或者將原任務替換爲一個新任務。主要的思路是,將延遲後實際執行的任務代碼進行替換,替換爲空閉包則至關於取消了任務,或者替換爲你想執行的其餘任務:

class AsyncAfter {

    typealias ExchangableTask = (_ newDelayTime: TimeInterval?,
        _ anotherTask:@escaping (() -> ())
        ) -> Void

    /// 延遲執行一個任務,並支持在實際執行前替換爲新的任務,並設定新的延遲時間。
    ///
    /// - Parameters:
    /// - time: 延遲時間
    /// - yourTask: 要執行的任務
    /// - Returns: 可替換原任務的閉包
    static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask {
        var exchangingTask: (() -> ())? // 備用替代任務
        var newDelayTime: TimeInterval? // 新的延遲時間

        let finalClosure = { () -> Void in
            if exchangingTask == nil {
                DispatchQueue.main.async(execute: yourTask)
            } else {
                if newDelayTime == nil {
                    DispatchQueue.main.async {
                        print("任務已更改,如今是:\(Date())")
                        exchangingTask!()
                    }
                }
                print("原任務取消了,如今是:\(Date())")
            }
        }

        dispatch_later(time) { finalClosure() }

        let exchangableTask: ExchangableTask =
        { delayTime, anotherTask in
            exchangingTask = anotherTask
            newDelayTime = delayTime

            if delayTime != nil {
                self.dispatch_later(delayTime!) {
                    anotherTask()
                    print("任務已更改,如今是:\(Date())")
                }
            }
        }

        return exchangableTask
    }
}
複製代碼

簡單說明一下:

delay 方法接收兩個參數,並返回一個閉包:

  • TimeInterval:延遲時間
  • @escaping () -> (): 要延遲執行的任務
  • 返回:可替換原任務的閉包,咱們去了一個別名:ExchangableTask

ExchangableTask 類型定義的閉包,接收一個新的延遲時間,和一個新的任務。

若是不執行返回的閉包,則在delay 方法內部,經過 dispatch_later 方法會繼續執行原任務。

若是執行了返回的 ExchangableTask 閉包,則會選擇執行新的任務。

代碼示例

本章對應的代碼見示例工程中 QueueTestListTableViewController+AsyncAfter.swift, AsyncAfter.swift.

示例 5.1:延遲執行任務,在計時結束前取消。

extension QueueTestListTableViewController {
    /// 延遲任務,在執行前臨時取消任務。
    @IBAction func ayncAfterCancelButtonTapped(_ sender: Any) {
        print("如今是:\(Date())")
        let task = AsyncAfter.delay(2) {
            print("打個電話 at: \(Date())")
        }

        // 當即取消任務
        task(0) {}
    }
}
複製代碼

根據咱們封裝的方法,只要提供一個空的閉包 {} 來替換原任務即至關於取消任務,同時還能夠指定取消的時間,task(0) {} 表示當即取消,task(nil) {} 表示按原計劃時間取消。

執行結果,能夠看到任務當即就被替換了,但延遲 2 秒的任務還在,只是變成了一個空任務:

如今是:2018-03-14 01:38:20 +0000
任務已更改,如今是:2018-03-14 01:38:20 +0000
原任務取消了,如今是:2018-03-14 01:38:22 +0000

示例 5.2:延遲執行任務,在執行前臨時替換爲新的任務。

extension QueueTestListTableViewController {
    @IBAction func ayncAfterNewTaskButtonTapped(_ sender: Any) {
        print("如今是:\(Date())")
        let task = AsyncAfter.delay(2) {
            print("打個電話 at: \(Date())")
        }

        // 3 秒後改成執行一個新任務
        task(3) {
            print("吃了個披薩,如今是:\(Date())")
        }
    }
}
複製代碼

執行結果,能夠看到 3 秒後執行了新的任務:

如今是:2018-03-14 03:14:08 +0000
原任務取消了,如今是:2018-03-14 03:14:10 +0000
吃了個披薩,如今是:2018-03-14 03:14:11 +0000
任務已更改,如今是:2018-03-14 03:14:11 +0000

6. 掛起和喚醒隊列

GCD 提供了一套機制,能夠掛起隊列中還沒有執行的任務,已經在執行的任務會繼續執行完,後續還能夠手動再喚醒隊列。

這兩個方法是屬於 DispatchObject 對象的方法,而這個對象是 DispatchQueueDispatchGroupDispatchSourceDispatchIODispatchSemaphore 這幾個類的父類,但這兩個方法只有 DispatchQueueDispatchSource 支持,調用時需注意。

掛起使用 suspend(),喚醒使用 resume()。對於隊列,這兩個方法調用時需配對,由於能夠屢次掛起,調用喚醒的次數應等於掛起的次數才能生效,喚醒的次數更多則會報錯,因此使用時最好設置一個計數器,或者封裝一個掛起、喚醒的方法,在方法內部進行檢查。

而對於 DispatchSource 則有所不一樣,它必須先調用 resume() 才能接收消息,因此此時喚醒的數量等於掛起的數量加一。

下面經過例子看看實現:

/// 掛起、喚醒測試類
class SuspendAndResum {
    let createQueueWithTask = CreateQueueWithTask()
    var concurrentQueue: DispatchQueue {
        return createQueueWithTask.concurrentQueue
    }
    var suspendCount = 0 // 隊列掛起的次數
    
    // MARK: ---------隊列方法------------
    
    /// 掛起測試
    func suspendQueue() {
        createQueueWithTask.printCurrentThread(with: "start test")
        concurrentQueue.async {
            self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task1")
        }
        concurrentQueue.async {
            self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task2")
        }
        
        // 經過柵欄掛起任務
        let barrierTask = DispatchWorkItem(flags: .barrier) {
            self.safeSuspend(self.concurrentQueue)
        }
        concurrentQueue.async(execute: barrierTask)
        
        concurrentQueue.async {
            self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task3")
        }
        
        concurrentQueue.async {
            self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task4")
        }
        concurrentQueue.async {
            self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task5")
        }
        createQueueWithTask.printCurrentThread(with: "end test")
    }
    
    /// 喚醒測試
    func resumeQueue() {
        self.safeResume(self.concurrentQueue)
    }
    
    /// 安全的掛失操做
    func safeSuspend(_ queue: DispatchQueue) {
        suspendCount += 1
        queue.suspend()
        print("任務掛起了")
    }
    
    /// 安全的喚醒操做
    func safeResume(_ queue: DispatchQueue) {
        if suspendCount == 1 {
            queue.resume()
            suspendCount = 0
            print("任務喚醒了")
        } else if suspendCount < 1 {
            print("喚醒的次數過多")
        } else {
            queue.resume()
            suspendCount -= 1
            print("喚醒的次數不夠,還須要 \(suspendCount) 次喚醒。")
        }
    }
    
}
複製代碼

經過按鈕調用測試:

let suspendAndResum = SuspendAndResum()

extension QueueTestListTableViewController {
    // 掛起
    @IBAction func suspendButtonTapped(_ sender: Any) {
        suspendAndResum.suspendQueue()
    }
    
    // 喚醒
    @IBAction func resumeButtonTapped(_ sender: Any) {
        suspendAndResum.resumeQueue()
    }
}
複製代碼

掛起的執行結果:

start test at thread: <NSThread: 0x17d357d0>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x17d357d0>{number = 1, name = main}, this is main thread
concurrentQueue async task1 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task2 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
任務掛起了

喚醒的執行結果:

任務喚醒了
concurrentQueue async task4 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task5 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task3 at thread: <NSThread: 0x17eae370>{number = 4, name = (null)}, this is not main thread

若是再按一次喚醒按鈕,則會提示:

喚醒的次數過多

7. 任務組

任務組至關於一系列任務的鬆散集合,它能夠來自相同或不一樣隊列,扮演着組織者的角色。它能夠通知外部隊列,組內的任務是否都已完成。或者阻塞當前的線程,直到組內的任務都完成。全部適合組隊執行的任務均可以使用任務組,且任務組更適合集合異步任務(若是都是同步任務,直接使用串行隊列便可)。

建立任務組

建立的方式至關簡單,無需任何參數:

let queueGroup = DispatchGroup()
複製代碼

將任務加入到任務組中

有兩種方式加入任務組:

  • 添加任務時指定任務組
let queue = DispatchQueue.global()
queue.async(group: queueGroup) {
    print("喝一杯牛奶")
}
複製代碼
  • 使用 Group.enter()Group.leave() 配對方法,標識任務加入任務組。
queueGroup.enter()
queue.async {
    print("吃一個蘋果")
    queueGroup.leave()
}
複製代碼

兩種加入方式在對任務處理的特性上是沒有區別的,只是便利之處不一樣。若是任務所在的隊列是本身建立或引用的系統隊列,那麼直接使用第一種方式直接加入便可。若是任務是由系統或第三方的 API 建立的,因爲沒法獲取到對應的隊列,只能使用第二種方式將任務加入組內,例如將 URLSessionaddDataTask 方法加入任務組中:

extension URLSession {
    func addDataTask(to group: DispatchGroup, with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
        -> URLSessionDataTask {
            group.enter() // 進入任務組
            return dataTask(with: request) { (data, response, error) in
                completionHandler(data, response, error)
                group.leave() // 離開任務組
            }
    }
}
複製代碼

任務組通知

等待任務組中的任務所有完成後,能夠統一對外發送通知,有兩種方式:

  • group.notify 方法,它能夠在全部任務完成後通知指定隊列並執行一個指定任務,這個通知的操做是異步的(意味着通知後續的代碼不須要等待任務,能夠繼續執行):
let group = DispatchGroup()

let queueBook = DispatchQueue(label: "book")
queueBook.async(group: group) {
    // do something 1
}
let queueVideo = DispatchQueue(label: "video")
queueVideo.async(group: group) {
    // do something 2
}

group.notify(queue: DispatchQueue.main) {
    print("all task done")
}

print("do something else.")

// 執行結果
// do something else.
// do something 1(任務 一、2 完成順序不固定)
// do something 2
// all task done
複製代碼
  • group.wait 方法,它會在全部任務完成後再執行當前線程中後續的代碼,所以這個操做是起到阻塞的做用:
let group = DispatchGroup()

let queueBook = DispatchQueue(label: "book")
queueBook.async(group: group) {
    // do something 1
}
let queueVideo = DispatchQueue(label: "video")
queueVideo.async(group: group) {
    // do something 2
}

group.wait()

print("do something else.")

// 執行結果
// do something 1(任務 一、2 完成順序不固定)
// do something 2
// do something else.
複製代碼

wait 方法中還能夠指定具體的時間,它表示將等待不超過這個時間,若是任務組在指定時間以內完成則當即恢復當前線程,不然將等到時間結束時再恢復當前線程。

  • 方式1,使用 DispatchTime,它表示一個時間間隔,精確到納秒(1/1000,000,000 秒):
let waitTime = DispatchTime.now() + 2.0 // 表示從當前時間開始後 2 秒,數字字面量也能夠改成使用 TimeInterval 類型變量
group.wait(timeout: waitTime)
複製代碼
  • 方式2,使用 DispatchWallTime,它表示當前的絕對時間戳,精確到微秒(1/1000,000 秒),一般使用字面量便可設置延時時間,也可使用 timespec 結構體來設置一個精確的時間戳,具體參見附錄章節的《時間相關的結構體說明 - DispatchWallTime》:
// 使用字面量設置
var wallTime = DispatchWallTime.now() + 2.0 // 表示從當前時間開始後 2 秒,數字字面量也能夠改成使用 TimeInterval 類型變量
複製代碼

代碼示例

本章對應的代碼見示例工程中 QueueTestListTableViewController+DispatchGroup.swift, DispatchGroup.swift.

示例 7.1:建立任務組,並以常規方式添加任務。

示例中咱們經過一個按鈕觸發,建立一個任務組、經過常規方式添加任務、任務完成時通知主線程。

extension QueueTestListTableViewController {
    @IBAction func creatTaskGroupButtonTapped(_ sender: Any) {
        let groupTest = DispatchGroupTest()
        let group = groupTest.creatAGroup()
        let queue = DispatchQueue.global()

        groupTest.addTaskNormally(to: group, in: queue)
        groupTest.notifyMainQueue(from: group)
    }
}

/// 任務組測試類,驗證任務組相關的特性。
class DispatchGroupTest {
    /// 建立一個新任務組
    func creatAGroup() -> DispatchGroup{
        return DispatchGroup()
    }

    /// 通知主線程任務組中的任務都完成
    func notifyMainQueue(from group: DispatchGroup) {
        group.notify(queue: DispatchQueue.main) {
            print("任務組通知:任務都完成了。\n")
        }
    }

    /// 建立常規的異步任務,並加入任務組中。
    func addTaskNormally(to group: DispatchGroup, in queue: DispatchQueue) {
        queue.async(group: group) {
            print("任務:喝一杯牛奶\n")
        }

        queue.async(group: group) {
            print("任務:吃一個蘋果\n")
        }
    }
}
複製代碼

執行結果:

任務:吃一個蘋果

任務:喝一杯牛奶

任務組通知:任務都完成了。

示例 7.2:添加系統任務至任務組

咱們經過封裝系統 SDK 中的 URLSessiondataTask API,將系統任務加入至任務組中,使用 Group.enter()Group.leave() 配對方法進行標識。

本示例中,咱們將經過封裝後的 API 嘗試從豆瓣同時下載兩本書的標籤集,當下載任務完成後返回一個打印任務的閉包,在主線程收到任務組所有完成的通知後,執行該打印閉包。

extension QueueTestListTableViewController {
    @IBAction func addSystemTaskToGroupButtonTapped(_ sender: Any) {
        let groupTest = DispatchGroupTest()
        let group = groupTest.creatAGroup()

        let book1ID = "5416832" // https://book.douban.com/subject/5416832/
        let book2ID = "1046265" // https://book.douban.com/subject/1046265/

        // 根據書籍 ID 下載一本豆瓣書籍的標籤集,並返回一個打印前 5 個標籤的任務閉包。
        let printBookTagBlock1 = groupTest.getBookTag(book1ID, in: group)
        let printBookTagBlock2 = groupTest.getBookTag(book2ID, in: group)

        // 下載任務完成後,通知主線程完成打印任務。
        groupTest.notifyMainQueue(from: group) {
            printBookTagBlock1("辛亥:搖晃的中國")
            printBookTagBlock2("挪威的森林")
        }
    }
}

class DispatchGroupTest {
    /// 根據書籍 ID 下載一本豆瓣書籍的標籤集,並返回一個打印前 5 個標籤的任務閉包。此任務將加入指定的任務組中執行。
    func getBookTag(_ bookID: String, in taskGroup: DispatchGroup) -> (String)->() {
        let url = "https://api.douban.com/v2/book/\(bookID)/tags"
        var printBookTagBlock: (_ bookName: String)->() = {_ in print("還未收到返回的書籍信息") }

        // 建立網絡信息獲取成功後的任務
        let completion = {(data: Data?, response: URLResponse?, error: Error?) in
            printBookTagBlock = { bookName in
                if error != nil{
                    print(error.debugDescription)
                } else {
                    guard let data = data else { return }
                    print("書籍 《\(bookName)》的標籤信息以下:")
                    BookTags.printBookPreviousFiveTags(data)
                }
            }
        }

        print("任務:下載書籍 \(bookID) 的信息 \(Date())")
        // 獲取網絡信息
        httpGet(url: url, in: taskGroup, completion: completion)

        let returnBlock: (String)->() = { bookName in
            printBookTagBlock(bookName)
        }
        return returnBlock
    }
}

/// 執行 http get 方法,並加入指定的任務組。
func httpGet(url: String, getString: String? = nil, session: URLSession = URLSession.shared, in taskGroup: DispatchGroup, completion: @escaping (Data?, URLResponse?, Error?) -> Void)
{
    let httpMethod = "GET"
    let urlStruct = URL(string: url) //建立URL對象
    var request = URLRequest(url: urlStruct!) //建立請求對象
    var dataTask: URLSessionTask

    request.httpMethod = httpMethod
    request.httpBody = getString?.data(using: .utf8)

    dataTask = session.addDataTask(to: taskGroup,
                                   with: request,
                                   completionHandler: completion)
    dataTask.resume() // 啓動任務
}

extension URLSession {
    /// 將數據獲取的任務加入任務組中
    func addDataTask(to group: DispatchGroup, with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
        -> URLSessionDataTask {
            group.enter()
            return dataTask(with: request) { (data, response, error) in
                print("下載結束:\(Date())")
                completionHandler(data, response, error)
                group.leave()
            }
    }
}
複製代碼

執行結果,能夠看到兩本書幾乎是同時開始下載的,所有下載結束後再進行打印:

任務:下載書籍 5416832 的信息 2018-03-13 03:21:29 +0000
任務:下載書籍 1046265 的信息 2018-03-13 03:21:29 +0000
下載結束:2018-03-13 03:21:30 +0000
下載結束:2018-03-13 03:21:30 +0000
任務組通知:任務都完成了。

書籍 《辛亥:搖晃的中國》的標籤信息以下:
歷史
張鳴
辛亥革命
民國
中國近代史

書籍 《挪威的森林》的標籤信息以下:
村上春樹
挪威的森林
小說
日本文學
日本

示例 7.3:添加系統及自定義任務至任務組

本示例中,咱們將先從豆瓣下載一本書的標籤集,並設置一個很短的等待時間,等待事後開啓打印任務。而後再加入一個自定義隊列的任務,以及裏一個書籍下載任務,當這兩個任務都完成後,再打印第二本書籍標籤信息。

extension QueueTestListTableViewController {
    @IBAction func addSystemTaskToGroupButtonTapped(_ sender: Any) {
        let groupTest = DispatchGroupTest()
        let group = groupTest.creatAGroup()
        let queue = DispatchQueue.global()

        let book1ID = "5416832" // https://book.douban.com/subject/5416832/
        let book2ID = "1046265" // https://book.douban.com/subject/1046265/

        // 根據書籍 ID 下載一本豆瓣書籍的標籤集,並返回一個打印前 5 個標籤的任務閉包。
        let printBookTagBlock1 = groupTest.getBookTag(book1ID, in: group)
        groupTest.wait(group: group, after: 0.01) // 等待前面的任務執行不超過 0.01 秒
        printBookTagBlock1("辛亥:搖晃的中國") // 等待後進行打印

        // 建立常規的異步任務,並加入任務組中。
        groupTest.addTaskNormally(to: group, in: queue)
        // 再次進行下載任務
        let printBookTagBlock2 = groupTest.getBookTag(book2ID, in: group)

        // 所有任務完成後,通知主線程完成打印任務。
        groupTest.notifyMainQueue(from: group) {
            printBookTagBlock2("挪威的森林")
        }
    }
}
複製代碼

執行結果,能夠看到因爲等待時間過短,第一本書還未下載完就開始打印了,所以只打印了空信息。而第二本書等待正常下載完再打印的:

任務:下載書籍 5416832 的信息 2018-03-13 03:42:21 +0000
還未收到返回的書籍信息
任務:喝一杯牛奶

任務:下載書籍 1046265 的信息 2018-03-13 03:42:21 +0000
任務:吃一個蘋果

下載結束:2018-03-13 03:42:22 +0000
下載結束:2018-03-13 03:42:22 +0000
任務組通知:任務都完成了。

書籍 《挪威的森林》的標籤信息以下:
村上春樹
挪威的森林
小說
日本文學
日本

8. DispatchSource

GCD 中提供了一個 DispatchSource 類,它能夠幫你監聽系統底層一些對象的活動,例如這些對象: Mach portUnix descriptorUnix signalVFS node,並容許你在這些活動發生時,向隊列提交一個任務以進行異步處理。

這些可監聽的對象都有具體的類型,你可使用 DispatchSource 的類方法來構建這些類型,這裏就不一一列舉了。下面以文件監聽爲例說明 DispatchSource 的用法。

例子中監聽了一個指定目錄下文件的寫入事件,建立監聽主要有幾個步驟:

  • 經過 makeFileSystemObjectSource 方法建立 source
  • 經過 setEventHandler 設定事件處理程序,setCancelHandler 設定取消監聽的處理。
  • 執行 resume() 方法開始接收事件
class DispatchSourceTest {
    var filePath: String
    var counter = 0
    let queue = DispatchQueue.global()
    
    init() {
        filePath = "\(NSTemporaryDirectory())"
        startObserve {
            print("File was changed")
        }
    }
    
    func startObserve(closure: @escaping () -> Void) {
        let fileURL = URL(fileURLWithPath: filePath)
        let monitoredDirectoryFileDescriptor = open(fileURL.path, O_EVTONLY)
        
        let source = DispatchSource.makeFileSystemObjectSource(
            fileDescriptor: monitoredDirectoryFileDescriptor,
            eventMask: .write, queue: queue)
        source.setEventHandler(handler: closure)
        source.setCancelHandler {
            close(monitoredDirectoryFileDescriptor)
        }
        source.resume()
    }
    
    func changeFile() {
        DispatchSourceTest.createFile(name: "DispatchSourceTest.md", filePath: NSTemporaryDirectory())
        counter += 1
        let text = "\(counter)"
        try! text.write(toFile: "\(filePath)/DispatchSourceTest.md", atomically: true, encoding: String.Encoding.utf8)
        print("file writed.")
    }
    
    static func createFile(name: String, filePath: String){
        let manager = FileManager.default
        let fileBaseUrl = URL(fileURLWithPath: filePath)
        let file = fileBaseUrl.appendingPathComponent(name)
        print("文件: \(file)")
        
        // 寫入 "hello world"
        let exist = manager.fileExists(atPath: file.path)
        if !exist {
            let data = Data(base64Encoded:"aGVsbG8gd29ybGQ=" ,options:.ignoreUnknownCharacters)
            let createSuccess = manager.createFile(atPath: file.path,contents:data,attributes:nil)
            print("文件建立結果: \(createSuccess)")
        }
    }
}
複製代碼

在 iOS 中這套 DispatchSource API 並不經常使用(DispatchSourceTimer 可能用的多點),並且僅上面的文件監聽例子常常接收不到事件,在 Mac 中狀況可能好點。對於須要常常和底層打交道的人來講,這裏面還有不少坑須要去填。 DispatchSource 的更多例子還能夠 參考這裏

9. DispatchIO

DispatchIO 對象提供一個操做文件描述符的通道。簡單講你能夠利用多線程異步高效地讀寫文件。

發起讀寫操做通常步驟以下:

  • 建立 DispatchIO 對象,或者說建立一個通道,並設置結束處理閉包。
  • 調用 read / write 方法
  • 調用 close 方法關閉通道
  • close 方法後系統將自動調用結束處理閉包

下面介紹下各方法的使用。

初始化方法

通常使用兩種方式初始化:文件描述符,或者文件路徑。

文件描述符方式

文件描述符使用 open方法建立:open(_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: mode_t) -> Int32,第一個參數是 UnsafePointer<Int8> 類型的路徑,oflagmode 指文件的操做權限,一個是系統 API 級的,一個是文件系統級的,可選項以下:

oflag

Flag 備註 功能
O_RDONLY 以只讀方式打開文件 此三種讀寫類型只能有一種
O_WRONLY 以只寫方式打開文件 此三種讀寫類型只能有一種
O_RDWR 以讀和寫的方式打開文件 此三種讀寫類型只能有一種
O_CREAT 打開文件,若是文件不存在則建立文件 建立文件時會使用Mode參數與Umask配合設置文件權限
O_EXCL 若是已經置O_CREAT且文件存在,則強制open()失敗 能夠用來檢測多個進程之間建立文件的原子操做
O_TRUNC 將文件的長度截爲0 不管打開方式是RD,WR,RDWR,只要打開就會把文件清空
O_APPEND 強制write()從文件尾開始不care當前文件偏移量所處位置,只會在文件末尾開始添加 若是不使用的話,只會在文件偏移量處開始覆蓋原有內容寫文件

mode:包含 User、Group、Other 三個組對應的權限掩碼。

User Group Other 說明
S_IRWXU S_IRWXG S_IRWXO 可讀、可寫、可執行
S_IRUSR S_IRGRP S_IROTH 可讀
S_IWUSR S_IWGR S_IWOTH 可寫
S_IXUSR S_IXGRP S_IXOTH 可執行

建立的通道有兩種類型:

  • 連續數據流:DispatchIO.StreamType.stream,這個方式是對文件從頭至尾完整操做的。
  • 隨機片斷數據:DispatchIO.StreamType.random,這個方式是在文件的任意一個位置(偏移量)開始操做的。
let filePath: NSString = "test.zip"
// 建立一個可讀寫的文件描述符
let fileDescriptor = open(filePath.utf8String!, (O_RDWR | O_CREAT | O_APPEND), (S_IRWXU | S_IRWXG))
let queue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue")
let cleanupHandler: (Int32) -> Void = { errorNumber in
}
let io = DispatchIO(type: .stream, fileDescriptor: fileDescriptor, queue: queue, cleanupHandler: cleanupHandler)
複製代碼

文件路徑方式

let io = DispatchIO(type: .stream, path: filePath.utf8String!, oflag: (O_RDWR | O_CREAT | O_APPEND), mode: (S_IRWXU | S_IRWXG), queue: queue, cleanupHandler: cleanupHandler)
複製代碼

數據塊大小閥值

DispatchIO 支持多線程操做的緣由之一就是它將文件拆分爲數據塊進行並行操做,你能夠設置數據塊大小的上下限,系統會採起合適的大小,使用這兩個方法便可:setLimit(highWater: Int)setLimit(lowWater: Int),單位是 byte

io.setLimit(highWater: 1024*1024)
複製代碼

數據塊若是設置小一點(如 1M),則能夠節省 App 的內存,若是內存足夠則能夠大一點換取更快速度。在進行讀寫操做時,有一個性能問題須要注意,若是同時讀寫的話通常分兩個通道,且讀到一個數據塊就當即寫到另外一個數據塊中,那麼寫通道的數據塊上限不要小於讀通道的,不然會形成內存大量積壓沒法及時釋放。

讀操做

方法示例:

ioRead.read(offset: 0, length: Int.max, queue: ioReadQueue) { doneReading, data, error in
        if (error > 0) {
            print("讀取發生錯誤了,錯誤碼:\(error)")
            return
        }
    if (data != nil) {
            // 使用數據
       }
    if (doneReading) {
           ioRead.close()
       }
}
複製代碼

offset 指定讀取的偏移量,若是通道是 stream 類型,值不起做用,寫爲 0 便可,將從文件開頭讀起;若是是 random 類型,則指相對於建立通道時文件的起始位置的偏移量。

length 指定讀取的長度,若是是讀取文件所有內容,設置 Int.max 便可,不然設置一個小於文件大小的值(單位是 byte)。

每讀取到一個數據塊都會調用你設置的處理閉包,系統會提供三個入參給你:結束標誌、本次讀取到的數據塊、錯誤碼:

  • 在全部數據讀取完成後,會額外再調用一個閉包,經過結束標誌告訴你操做結束了,此時 data 大小是 0,錯誤碼也是 0。
  • 若是讀取中間發生了錯誤,則會中止讀取,結束標誌會被設置爲 true,並返回相應的錯誤碼,錯誤碼錶參考稍後的【關閉通道】小節:

寫操做

方法示例:

ioWrite.write(offset: 0, data: data!, queue: ioWriteQueue) { doneWriting, data, error in
    if (error > 0) {
        print("寫入發生錯誤了,錯誤碼:\(error)")
        return
    }
    if doneWriting {
        //...
        ioWrite.close()
    }
}
複製代碼

寫操做與讀操做的惟一區別是:每當寫完一個數據塊時,回調閉包返回的 data 是剩餘的所有數據。同時注意若是是 stream 類型,將接着文件的末尾寫數據。

關閉通道

當讀寫正常完成,或者你須要中途結束操做時,須要調用 close 方法,這個方法帶一個 DispatchIO.CloseFlags 類型參數,若是不指定將默認值爲 DispatchIO.CloseFlags.stop

這個方法傳入 stop 標誌時將會中止全部未完成的讀寫操做,影響範圍是全部 I/O channel,其餘 DispatchIO 對象進行中的讀寫操做將會收到一個 ECANCELED 錯誤碼,rawValue 值是 89,這個錯誤碼是 POSIXError 結構的一個屬性,而 POSIXError 又是 NSError 中預約義的一個錯誤域。

所以若是要在不一樣 DispatchIO 對象中並行讀取操做互不影響, close 方法標誌能夠設置一個空值:DispatchIO.CloseFlags()。若是設置了 stop 標誌,則要作好不一樣 IO 之間的隔離,經過任務組的enterleavewait 方法能夠作到較好的隔離。

ioWrite.close() // 中止標誌
ioWrite.close(flags: DispatchIO.CloseFlags()) // 空標誌
複製代碼

POSIXError 碼錶:

EPERM  = 1  // 無
ENOENT  = 2  // No such file or directory.
ESRCH  = 3  // No such process.
EINTR  = 4  // Interrupted system call.
EIO  = 5  // Input/output error.
ENXIO  = 6  // Device not configured.
E2BIG  = 7  // Argument list too long.
ENOEXEC  = 8  // Exec format error.
EBADF  = 9  // Bad file descriptor.
ECHILD  = 10  // No child processes.
EDEADLK  = 11  // Resource deadlock avoided.
ENOMEM  = 12  // Cannot allocate memory.
EACCES  = 13  // Permission denied.
EFAULT  = 14  // Bad address.
ENOTBLK  = 15  // Block device required.
EBUSY  = 16  // Device / Resource busy.
EEXIST  = 17  // File exists.
EXDEV  = 18  // Cross-device link.
ENODEV  = 19  // Operation not supported by device.
ENOTDIR  = 20  // Not a directory.
EISDIR  = 21  // Is a directory.
EINVAL  = 22  // Invalid argument.
ENFILE  = 23  // Too many open files in system.
EMFILE  = 24  // Too many open files.
ENOTTY  = 25  // Inappropriate ioctl for device.
ETXTBSY  = 26  // Text file busy.
EFBIG  = 27  // File too large.
ENOSPC  = 28  // No space left on device.
ESPIPE  = 29  // Illegal seek.
EROFS  = 30  // Read-only file system.
EMLINK  = 31  // Too many links.
EPIPE  = 32  // Broken pipe.
EDOM  = 33  // math software. Numerical argument out of domain.
ERANGE  = 34  // Result too large.
EAGAIN  = 35  // non-blocking and interrupt i/o. Resource temporarily unavailable.
EWOULDBLOCK  = 35  // Operation would block.
EINPROGRESS  = 36  // Operation now in progress.
EALREADY  = 37  // Operation already in progress.
ENOTSOCK  = 38  // ipc/network software – argument errors. Socket operation on non-socket.
EDESTADDRREQ  = 39  // Destination address required.
EMSGSIZE  = 40  // Message too long.
EPROTOTYPE  = 41  // Protocol wrong type for socket.
ENOPROTOOPT  = 42  // Protocol not available.
EPROTONOSUPPORT  = 43  // Protocol not supported.
ESOCKTNOSUPPORT  = 44  // Socket type not supported.
ENOTSUP  = 45  // Operation not supported.
EPFNOSUPPORT  = 46  // Protocol family not supported.
EAFNOSUPPORT  = 47  // Address family not supported by protocol family.
EADDRINUSE  = 48  // Address already in use.
EADDRNOTAVAIL  = 49  // Can’t assign requested address.
ENETDOWN  = 50  // ipc/network software – operational errors Network is down.
ENETUNREACH  = 51  // Network is unreachable.
ENETRESET  = 52  // Network dropped connection on reset.
ECONNABORTED  = 53  // Software caused connection abort.
ECONNRESET  = 54  // Connection reset by peer.
ENOBUFS  = 55  // No buffer space available.
EISCONN  = 56  // Socket is already connected.
ENOTCONN  = 57  // Socket is not connected.
ESHUTDOWN  = 58  // Can’t send after socket shutdown.
ETOOMANYREFS  = 59  // Too many references: can’t splice.
ETIMEDOUT  = 60  // Operation timed out.
ECONNREFUSED  = 61  // Connection refused.
ELOOP  = 62  // Too many levels of symbolic links.
ENAMETOOLONG  = 63  // File name too long.
EHOSTDOWN  = 64  // Host is down.
EHOSTUNREACH  = 65  // No route to host.
ENOTEMPTY  = 66  // Directory not empty.
EPROCLIM  = 67  // quotas & mush. Too many processes.
EUSERS  = 68  // Too many users.
EDQUOT  = 69  // Disc quota exceeded.
ESTALE  = 70  // Network File System. Stale NFS file handle.
EREMOTE  = 71  // Too many levels of remote in path.
EBADRPC  = 72  // RPC struct is bad.
ERPCMISMATCH  = 73  // RPC version wrong.
EPROGUNAVAIL  = 74  // RPC prog. not avail.
EPROGMISMATCH  = 75  // Program version wrong.
EPROCUNAVAIL  = 76  // Bad procedure for program.
ENOLCK  = 77  // No locks available.
ENOSYS  = 78  // Function not implemented.
EFTYPE  = 79  // Inappropriate file type or format.
EAUTH  = 80  // Authentication error.
ENEEDAUTH  = 81  // Need authenticator.
EPWROFF  = 82  // Intelligent device errors. Device power is off.
EDEVERR  = 83  // Device error e.g. paper out.
EOVERFLOW  = 84  // Value too large to be stored in data type.
EBADEXEC  = 85  // Program loading errors. Bad executable.
EBADARCH  = 86  // Bad CPU type in executable.
ESHLIBVERS  = 87  // Shared library version mismatch.
EBADMACHO  = 88  // Malformed Macho file.
ECANCELED  = 89  // Operation canceled.
EIDRM  = 90  // Identifier removed.
ENOMSG  = 91  // No message of desired type.
EILSEQ  = 92  // Illegal byte sequence.
ENOATTR  = 93  // Attribute not found.
EBADMSG  = 94  // Bad message.
EMULTIHOP  = 95  // Reserved.
ENODATA  = 96  // No message available on STREAM.
ENOLINK  = 97  // Reserved.
ENOSR  = 98  // No STREAM resources.
ENOSTR  = 99  // Not a STREAM.
EPROTO  = 100  // Protocol error.
ETIME  = 101  // STREAM ioctl timeout.
ENOPOLICY  = 103  // No such policy registered.
ENOTRECOVERABLE  = 104  // State not recoverable.
EOWNERDEAD  = 105  // Previous owner died.
EQFULL  = 106  // Interface output queue is full.
複製代碼

代碼示例

示例 9.1:將兩個大文件(經過壓縮工具拆分的包)合併爲一個文件。

實現思路:分別建立一個讀、寫通道,使用同一個串行隊列處理數據,每讀到一個數據塊就提交一個寫數據的任務,同時要保證按照讀取的順序提交寫任務,在第一個文件讀寫完成後再開始第二個文件的讀寫操做。

測試文件地址:WWDC 2016-720,經過 Zip 壓縮拆分爲兩個文件(Normal 方式),設置按 350M 進行分割。注意測試時,建議使用模擬器,更方便讀寫 Mac 本地文件,後續相似例子相同。

class DispatchIOTest {
    /// 利用很小的內存空間及同一隊列讀寫方式合併文件
    static func combineFileWithOneQueue() {
        let files: NSArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001",
                              "/Users/xxx/Downloads/gcd.mp4.zip.002"]
        let outFile: NSString = "/Users/xxx/Downloads/gcd.mp4.zip"
        let ioQueue = DispatchQueue(
            label: "com.sinkingsoul.DispatchQueueTest.serialQueue")
        let queueGroup = DispatchGroup()
        
        let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber in
            print("寫入文件完成 @\(Date())。")
        }
        
        let ioReadCleanupHandler: (Int32) -> Void = { errorNumber in
            print("讀取文件完成。")
        }
        
        let ioWrite = DispatchIO(type: .stream,
                                 path: outFile.utf8String!,
                                 oflag: (O_RDWR | O_CREAT | O_APPEND),
                                 mode: (S_IRWXU | S_IRWXG),
                                 queue: ioQueue,
                                 cleanupHandler: ioWriteCleanupHandler)
        ioWrite?.setLimit(highWater: 1024*1024)
        
// print("開始操做 @\(Date()).")
        
        files.enumerateObjects { fileName, index, stop in
            if stop.pointee.boolValue {
                return
            }
            queueGroup.enter()
            
            let ioRead = DispatchIO(type: .stream,
                                    path: (fileName as! NSString).utf8String!,
                                    oflag: O_RDONLY,
                                    mode: 0,
                                    queue: ioQueue,
                                    cleanupHandler: ioReadCleanupHandler)
            ioRead?.setLimit(highWater: 1024*1024)
            
            print("開始讀取文件: \(fileName) 的數據")
            
            ioRead?.read(offset: 0, length: Int.max, queue: ioQueue) { doneReading, data, error in
                print("當前讀線程:\(Thread.current)--->")
                if (error > 0 || stop.pointee.boolValue) {
                    print("讀取發生錯誤了,錯誤碼:\(error)")
                    ioWrite?.close()
                    stop.pointee = true
                    return
                }
                
                if (data != nil) {
                    let bytesRead: size_t = data!.count
                    if (bytesRead > 0) {
                        queueGroup.enter()
                        ioWrite?.write(offset: 0, data: data!, queue: ioQueue) {
                            doneWriting, data, error in
                            print("當前寫線程:\(Thread.current)--->")
                            if (error > 0 || stop.pointee.boolValue) {
                                print("寫入發生錯誤了,錯誤碼:\(error)")
                                ioRead?.close()
                                stop.pointee = true
                                queueGroup.leave()
                                return
                            }
                            if doneWriting {
                                queueGroup.leave()
                            }
                            print("--->當前寫線程:\(Thread.current)")
                        }
                    }
                }
                
                if (doneReading) {
                    ioRead?.close()
                    if (files.count == (index+1)) {
                        ioWrite?.close()
                    }
                    queueGroup.leave()
                }
                print("--->當前讀線程:\(Thread.current)")
            }
            _ = queueGroup.wait(timeout: .distantFuture)
        }
    }
}
複製代碼

執行結果,能夠看到串行隊列利用了好幾個線程來處理讀寫操做,可是細看同一時間只運行了一個線程,符合咱們前面總結的串行隊列的特色:

開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 的數據
當前讀線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前讀線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}--->
--->當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}
當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}--->
--->當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}

......

當前讀線程:<NSThread: 0x600000662840>{number = 6, name = (null)}--->
--->當前讀線程:<NSThread: 0x600000662840>{number = 6, name = (null)}
當前寫線程:<NSThread: 0x600000662ac0>{number = 7, name = (null)}--->
--->當前寫線程:<NSThread: 0x600000662ac0>{number = 7, name = (null)}

......

讀取文件完成。

......

當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
寫入文件完成。

關閉 print 後的內存佔用狀況見下圖,能夠看到在讀寫過程當中只額外佔用了 1M 左右內存,用時 1s 左右,很是的棒。

開始操做 @2018-xx-xx 13:51:52 +0000.
開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 的數據
讀取文件完成。
開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002 的數據
讀取文件完成。
寫入文件完成 @2018-xx-xx 13:51:53 +0000。

示例 9.2:利用多個隊列將兩個大文件合併爲一個文件。

這個例子在上面例子的基礎上,各使用兩個隊列來進行讀、寫操做,驗證利用地址偏移的方式多線程同時讀寫文件的效率。

這裏對讀寫文件時的偏移量 offset 再作個簡單說明:文件開頭的偏移量是 0,後續逐漸遞增,直到文件末尾的偏移量是 (按字節計算的文件大小 - 1)。

/// 利用很小的內存空間及雙隊列讀寫方式合併文件
static func combineFileWithMoreQueues() {
    let files: NSArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001",
                          "/Users/xxx/Downloads/gcd.mp4.zip.002"]
// 真機運行時可以使用如下地址(需手動將文件放入工程中)
// let files: NSArray = [Bundle.main.path(forResource: "gcd.mp4.zip", ofType: "001")!,
// Bundle.main.path(forResource: "gcd.mp4.zip", ofType: "002")!]
    var filesSize = files.map {
        return (try! FileManager.default.attributesOfItem(atPath: $0 as! String)[FileAttributeKey.size] as! NSNumber).int64Value
    }
    let outFile: NSString = "/Users/xxx/Downloads/gcd.mp4.zip"
// 真機運行時可以使用如下地址(需手動將文件放入工程中)
// let outFile: NSString = "\(NSTemporaryDirectory())/gcd.mp4.zip" as NSString

    // 每一個分塊文件各一個讀、寫隊列
    let ioReadQueue1 = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue1")
    let ioReadQueue2 = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue2")
    let ioWriteQueue1 = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue3")
    let ioWriteQueue2 = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue4")

    let ioReadQueueArray = [ioReadQueue1, ioReadQueue2]
    let ioWriteQueueArray = [ioWriteQueue1, ioWriteQueue2]
    let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber in
        print("寫入文件完成 @\(Date())。")
    }
    let ioReadCleanupHandler: (Int32) -> Void = { errorNumber in
        print("讀取文件完成 @\(Date())。")
    }

    let queueGroup = DispatchGroup()

    print("開始操做 @\(Date()).")

    let ioWrite = DispatchIO(type: .random, path: outFile.utf8String!, oflag: (O_RDWR | O_CREAT | O_APPEND), mode: (S_IRWXU | S_IRWXG), queue: ioWriteQueue1, cleanupHandler: ioWriteCleanupHandler)
    ioWrite?.setLimit(highWater: 1024 * 1024)
    ioWrite?.setLimit(lowWater: 1024 * 1024)

    filesSize.insert(0, at: 0)
    filesSize.removeLast()

    for (index, file) in files.enumerated() {
        DispatchQueue.global().sync {
            queueGroup.enter()

            let ioRead = DispatchIO(type: .stream, path: (file as! NSString).utf8String!, oflag: O_RDONLY, mode: 0, queue: ioReadQueue1, cleanupHandler: ioReadCleanupHandler)
            ioRead?.setLimit(highWater: 1024 * 1024)
            ioRead?.setLimit(lowWater: 1024 * 1024)

            var writeOffsetTemp = filesSize[0...index].reduce(0) { offset, size in
                return offset + size
            }

            ioRead?.read(offset: 0, length: Int.max, queue: ioReadQueueArray[index]) {
                doneReading, data, error in
// print("讀取文件: \(file),線程:\(Thread.current)--->")
                if (error > 0) {
                    print("讀取文件: \(file) 發生錯誤了,錯誤碼:\(error)")
                    return
                }

                if (doneReading) {
                    ioRead?.close()
                    queueGroup.leave()
                }

                if (data != nil) {
                    let bytesRead: size_t = data!.count
                    if (bytesRead > 0) {
                        queueGroup.enter()
                        ioWrite?.write(offset: writeOffsetTemp, data: data!, queue: ioWriteQueueArray[index]) {
                            doneWriting, writeData, error in
// print("寫入文件: \(file), 線程:\(Thread.current)--->")
                            if (error > 0) {
                                print("寫入文件: \(file) 發生錯誤了,錯誤碼:\(error)")
                                ioRead?.close()
                                return
                            }
                            if doneWriting {
                                queueGroup.leave()
                            }
// print("--->寫入文件: \(file), 線程:\(Thread.current)")
                        }
                        writeOffsetTemp = writeOffsetTemp + Int64(data!.count)
                    }
                }
// print("--->讀取文件: \(file) ,線程:\(Thread.current)")
            }
        }
    }
    _ = queueGroup.wait(timeout: .distantFuture)
    ioWrite?.close()

}
複製代碼

執行結果,能夠看到 4 個串行隊列同時都在運行:

開始操做 @2018-04-03 03:57:00 +0000.
讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001,線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}--->
讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002,線程:<NSThread: 0x60400047d9c0>{number = 4, name = (null)}--->
--->讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002 ,線程:<NSThread: 0x60400047d9c0>{number = 4, name = (null)}
--->讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 ,線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}

......

寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}--->
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.002, 線程:<NSThread: 0x600000672980>{number = 5, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.002, 線程:<NSThread: 0x600000672980>{number = 5, name = (null)}

......

寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
讀取文件完成 @2018-04-03 03:57:04 +0000。
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
讀取文件完成 @2018-04-03 03:57:04 +0000。
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}

......

寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件完成 @2018-04-03 03:57:04 +0000。

關閉 print 後的內存佔用狀況見下圖,能夠看到在讀寫過程當中額外佔用了 3M 左右內存,用時 2s 左右。這個結果中,內存佔用比單隊列大(這個比較好理解),但速度還更慢了,性能瓶頸頗有多是在磁盤讀寫上。因此涉及文件寫操做時,並非線程越多越快,要考慮傳輸速度、文件大小等因素。

開始操做 @2018-04-03 04:05:44 +0000.
讀取文件完成 @2018-04-03 04:05:45 +0000。
讀取文件完成 @2018-04-03 04:05:46 +0000。
寫入文件完成 @2018-04-03 04:05:46 +0000。

10. DispatchData

DispatchData 對象能夠管理基於內存的數據緩衝區。這個數據緩衝區對外表現爲連續的內存區域,但內部可能由多個獨立的內存區域組成。

DispatchData 對象不少特性相似於 Data 對象,且 Data 對象能夠轉換爲 DispatchData 對象,而經過 DispatchIOread 方法得到的數據也是封裝爲 DispatchData 對象的。

下面再看個示例,經過 DataDispatchDataDispatchIO 這三種類型結合,完成內存佔用更小也一樣快速的文件讀寫操做。

代碼示例

示例 10.1:將兩個大文件合併爲一個文件(與示例 9.1 相似)。

實現思路:首先將兩個文件轉換爲 Data 對象,再轉換爲 DispatchData 對象,而後拼接兩個對象爲一個 DispatchData 對象,最後經過 DispatchIOwrite 方法寫入文件中。看起來有屢次的轉換過程,實際上 Data 類型讀取文件時支持虛擬隱射的方式,而 DispatchData 類型更是支持多個數據塊虛擬拼接,也不佔用什麼內存。

實際上徹底使用 Data 類型也能完成文件合併,利用 appendwrite 方法便可,可是 append 方法是要佔用比文件大小稍大的內存,write 方法也要佔用額外內存空間。即便使用 NSMutableData 類型不佔用內存的 append 方法經過虛擬隱射方式讀文件(即讀文件、拼接數據都不佔用內存),可是 NSMutableData 類型的 write 方法仍是要佔用額外內存,雖然要比 Data 類型內存少不少,可是也很多了。所以 DispatchData 類型在內存佔用上更有優點。

/// 利用 DispatchData 類型快速合併文件
static func combineFileWithDispatchData() {
    let filePathArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001",
                          "/Users/xxx/Downloads/gcd.mp4.zip.002"]
    let outputFilePath: NSString = "/Users/xxx/Downloads/gcd.mp4.zip"
    let ioWriteQueue = DispatchQueue(
        label: "com.sinkingsoul.DispatchQueueTest.serialQueue")
    
    let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber in
        print("寫入文件完成 @\(Date()).")
    }
    let ioWrite = DispatchIO(type: .stream,
                             path: outputFilePath.utf8String!,
                             oflag: (O_RDWR | O_CREAT | O_APPEND),
                             mode: (S_IRWXU | S_IRWXG),
                             queue: ioWriteQueue,
                             cleanupHandler: ioWriteCleanupHandler)
    ioWrite?.setLimit(highWater: 1024*1024*2)
    
    print("開始操做 @\(Date()).")
    
    // 將全部文件合併爲一個 DispatchData 對象
    let dispatchData = filePathArray.reduce(DispatchData.empty) { data, filePath in
        // 將文件轉換爲 Data
        let url = URL(fileURLWithPath: filePath)
        let fileData = try! Data(contentsOf: url, options: .mappedIfSafe)
        var tempData = data
        // 將 Data 轉換爲 DispatchData
        let dispatchData = fileData.withUnsafeBytes {
            (u8Ptr: UnsafePointer<UInt8>) -> DispatchData in
            let rawPtr = UnsafeRawPointer(u8Ptr)
            let innerData = Unmanaged.passRetained(fileData as NSData)
            return DispatchData(bytesNoCopy:
                UnsafeRawBufferPointer(start: rawPtr, count: fileData.count),
                                deallocator: .custom(nil, innerData.release))
        }
        // 拼接 DispatchData
        tempData.append(dispatchData)
        return tempData
    }
    
    //將 DispatchData 對象寫入結果文件中
    ioWrite?.write(offset: 0, data: dispatchData, queue: ioWriteQueue) {
        doneWriting, data, error in
        if (error > 0) {
            print("寫入發生錯誤了,錯誤碼:\(error)")
            return
        }
        
        if data != nil {
// print("正在寫入文件,剩餘大小:\(data!.count) bytes.")
        }
        
        if (doneWriting) {
            ioWrite?.close()
        }
    }
}
複製代碼

執行結果:

開始操做 @2018-xx-xx 13:32:37 +0000.
正在寫入文件,剩餘大小:640096267 bytes.
正在寫入文件,剩餘大小:639047691 bytes.
......
正在寫入文件,剩餘大小:464907 bytes.
寫入文件完成 @2018-xx-xx 13:32:40 +0000.

關閉 print 後的內存佔用狀況見下圖,能夠看到在整個讀寫過程當中幾乎沒有額外佔用內存,速度很快在 1s 左右,這個讀寫方案堪稱完美,這要歸功於 DispatchData 的虛擬拼接和 DispatchIO 的分塊讀寫大小控制。這裏順便提一下 DispatchIO 數據閥值上限 highWater,通過測試,若是設置爲 1M,將耗時 4s 左右,設爲 2M 及以上時,耗時均爲 1s 左右,很是快速,而全部閥值的內存佔用都不多。因此設置合理的閥值,對性能的改善也是有幫助的。

11. 信號量

DispatchSemaphore,一般稱做信號量,顧名思義,它能夠經過計數來標識一個信號,這個信號怎麼用呢,取決於任務的性質。一般用於對同一個資源訪問的任務數進行限制。

例如,控制同一時間寫文件的任務數量、控制端口訪問數量、控制下載任務數量等。

信號量的使用很是的簡單:

  • 首先建立一個初始數量的信號對象
  • 使用 wait 方法讓信號量減 1,再安排任務。若是此時信號量仍大於或等於 0,則任務可執行,若是信號量小於 0,則任務須要等待其餘地方釋放信號。
  • 任務完成後,使用 signal 方法增長一個信號量。
  • 等待信號有兩種方式:永久等待、可超時的等待。

下面看個簡單的例子

代碼示例

示例 11.1:限制同時運行的任務數。

/// 信號量測試類
class DispatchSemaphoreTest {
    
    /// 限制同時運行的任務數
    static func limitTaskNumber() {
        let queue = DispatchQueue(
            label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue",
            attributes: .concurrent)
        let semaphore = DispatchSemaphore(value: 2) // 設置數量爲 2 的信號量
        
        semaphore.wait()
        queue.async {
            task(index: 1)
            semaphore.signal()
        }
        
        semaphore.wait()
        queue.async {
            task(index: 2)
            semaphore.signal()
        }
        
        semaphore.wait()
        queue.async {
            task(index: 3)
            semaphore.signal()
        }
    }
    
    /// 任務
    static func task(index: Int) {
        print("Begin task \(index) --->")
        Thread.sleep(forTimeInterval: 2)
        print("Sleep for 2 seconds in task \(index).")
        print("--->End task \(index).")
    }

}
複製代碼

執行結果,示例中設置了同時只能運行 2 個任務,能夠看到任務 3 在前兩個任務完成後纔開始運行:

Begin task 2 --->
Begin task 1 --->
Sleep for 2 seconds in task 2.
Sleep for 2 seconds in task 1.
--->End task 2.
--->End task 1.
Begin task 3 --->
Sleep for 2 seconds in task 3.
--->End task 3.

12. 任務對象

在隊列和任務組中,任務其實是被封裝爲一個 DispatchWorkItem 對象的。任務封裝最直接的好處就是能夠取消任務。

前面提到的柵欄任務就是經過封裝任務對象實現的。

建立任務

先看看它的建立,其中 qosflags 參數都有默認值,能夠不填:

let workItem = DispatchWorkItem(qos: .default, flags: DispatchWorkItemFlags()) {
    // Do something
}
複製代碼

qos 前面提到過了,這裏說一下 DispatchWorkItemFlags,它有如下幾個靜態屬性(詳細解釋可參考 官方源碼 ):

  • assignCurrentContext: 標記應該爲任務分配建立它時的上下文屬性(例如:QoS、os_activity_t、可能存在的當前 IPC 請求屬性)。若是直接調用任務,任務對象將在它的持續時間內在調用線程中應用這些屬性。若是提交任務至隊列中,則會替換提交任務時的上下文屬性默認值。
  • barrier: 標記任務爲柵欄任務,提交至並行隊列時生效,若是直接運行該任務對象則無此效果。
  • detached: 標記任務在執行時應該剝離當前執行上下文屬性(例如:QoS、os_activity_t、可能存在的當前 IPC 請求屬性)。若是直接調用任務,任務對象將在它的持續時間內從調用線程中刪除這些屬性(若是存在屬性,且應用於任務以前)。若是提交任務至隊列中,將使用隊列的屬性(或專門分配給任務對象的任何屬性)進行執行。若是建立任務時指定了 QoS,則該 QoS 將優先於 flag 對應的 QoS 值。
  • enforceQoS: 標記任務提交至隊列執行時,任務對象被分配的 QoS (提交任務時的值)應優先於隊列的 QoS,這樣作不會下降 QoS。當任務提交至隊列同步執行時,或則直接執行任務時,這個 flag 是默認值。
  • inheritQoS: 標記任務提交至隊列執行時,隊列的 QoS 應優先於任務對象被分配的 QoS (提交任務時的值),後一個 QoS 值只會在隊列的 QoS 有問題時纔會採用,這樣作會致使 QoS 不會低於繼承自隊列的 QoS。當任務提交至隊列異步執行時,這個 flag 是默認值,且直接執行任務時該標誌無效。
  • noQoS: 標記任務不該指定 QoS,若是直接執行,將以調用線程的 QoS 執行。若是提交至隊列,則會替換提交任務時的 QoS 默認值。

執行任務

執行任務時,調用任務項對象的 perform() 方法,這個調用是同步執行的:

workItem.perform()
複製代碼

或則在隊列中執行:

let queue = DispatchQueue.global()
queue.async(execute: workItem)
複製代碼

取消任務

在任務未實際執行以前能夠取消任務,調用 cancel() 方法,這個調用是異步執行的:

workItem.cancel()
複製代碼

取消任務將會帶來如下結果:

  • 取消將致使 任何 未來的任務在執行時當即返回,但不會影響已在執行的任務。
  • 與任務對象關聯的任何資源的釋放都會延遲,直到下一次嘗試執行任務對象(或者任何正在進行中的執行已完成)。所以須要注意確保可能被取消的任務對象不要捕獲任何須要實際執行才能釋放的資源,例如使用 malloc(3) 進行內存分配,而在任務中調用 free(3) 釋放。 若是因爲取消而從未執行任務,則會致使內存泄露。

任務通知

任務對象也有一個通知方法,在任務執行完成後能夠向指定隊列發送一個異步調用閉包:

workItem.notify(queue: queue) {
    // Do something
}
複製代碼

這個通知方法有一些地方須要注意:

  • 任務不支持在被屢次調用結束後再發出通知,運行時將會報錯,通知只能響應一次完整的調用(若是在發出通知時,還有另外一次執行未完成,這種狀況也視爲只有一次調用)。須要在屢次執行結束後發出通知,使用任務組的通知更合適。
  • 能夠屢次發出通知,但通知執行的順序是不肯定的。
  • 任務只要提交至隊列中,即便調用 cancel() 方法被取消了,通知也能夠生效。

任務等待

任務對象支持等待方法,相似於任務組的等待,也是阻塞型的,須要等待已有的任務完成才能繼續執行,也能夠指定等待時間:

workItem.perform()
workItem.wait()
workItem.wait(timeout: DispatchTime) // 指定等待時間
workItem.wait(wallTimeout: DispatchWallTime) // 指定等待時間
// 等待任務完成
// do something
複製代碼

下面看個完整的例子:

代碼示例

示例 12.1:任務對象測試。

/// 任務對象測試
@IBAction func dispatchWorkItemTestButtonTapped(_ sender: Any) {
    DispatchWorkItemTest.workItemTest()
}

/// 任務對象測試類
class DispatchWorkItemTest {
    static func workItemTest() {
        var value = 10
        let workItem = DispatchWorkItem {
            print("workItem running start.--->")
            value += 5
            print("value = ", value)
            print("--->workItem running end.")
        }
        let queue = DispatchQueue.global()
        
        queue.async(execute: workItem)
        
        queue.async {
            print("異步執行 workItem")
            workItem.perform()
            print("任務2取消了嗎:\(workItem.isCancelled)")
            workItem.cancel()
            print("異步執行 workItem end")
        }
        
        workItem.notify(queue: queue) {
            print("notify 1: value = ", value)
        }
    
        workItem.notify(queue: queue) {
            print("notify 2: value = ", value)
        }
        
        workItem.notify(queue: queue) {
            print("notify 3: value = ", value)
        }
        
        queue.async {
            print("異步執行2 workItem")
            Thread.sleep(forTimeInterval: 2)
            print("任務3取消了嗎:\(workItem.isCancelled)")
            workItem.perform()
            print("異步執行2 workItem end")
        }
    }

}
複製代碼

執行結果,能夠看到任務第一次執行完成後,發出了 3 次通知,並且未按照代碼的順序。在發出通知前,任務還有一次執行未完成,並未形成通知報錯。第二次執行任務後,取消了任務,所以任務第三次未正常執行:

workItem running start.--->
異步執行 workItem
異步執行2 workItem
value = 15
workItem running start.--->
value = 20
--->workItem running end.
任務2取消了嗎:false
異步執行 workItem end
notify 2: value = 20
notify 3: value = 20
notify 1: value = 20
--->workItem running end.
任務3取消了嗎:true
異步執行2 workItem end

附:時間相關的結構體說明

DispatchTime

它經過時間間隔的方式來表示一個時間點,初始時間從系統最近一次開機時間開始計算,並且在系統休眠時暫停計時,等系統恢復後繼續計時,精確到納秒(1/1000,000,000 秒)。能夠直接使用 + 運算符設定延時,若是使用變量延時要使用 TimeInterval 類型:

DispatchTime.now() // 表示當前時間與開機時間的間隔

let twoSecondAfter = DispatchTime.now() + 2.1 // 當前時間以後 2.1 秒
複製代碼

DispatchWallTime

它表示一個絕對時間的時間戳,能夠直接使用字面量表示延時,也能夠借用 timespec 結構體來表示,以微秒爲單位(1/1000,000 秒)。

// 使用字面量設置
var wallTime = DispatchWallTime.now() + 2.0 // 表示從當前時間開始後 2 秒,數字字面量也能夠改成使用 TimeInterval 類型變量

// 獲取當前時間,以 timeval 結構體的方式表示
var getTimeval = timeval()
gettimeofday(&getTimeval, nil)

// 轉換爲 timespec 結構體
let time = timespec(tv_sec: __darwin_time_t(getTimeval.tv_sec), tv_nsec: Int(getTimeval.tv_usec * 1000))

// 轉換爲 DispatchWallTime
let wallTime = DispatchWallTime(timespec: time)
複製代碼

如何經過字符串字面量建立 DispatchWallTime 時間戳

首先須要作一些擴展:

extension Date {
    /// 經過字符串字面量建立 DispatchWallTime 時間戳
    ///
    /// - Parameter dateString: 時間格式字符串,如:"2016-10-05 13:11:12"
    /// - Returns: DispatchWallTime 時間戳
    static func getWallTime(from dateString: String) -> DispatchWallTime? {
        let dateformatter = DateFormatter()
        dateformatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
        dateformatter.timeZone = TimeZone(secondsFromGMT: 0)

        var newDate = dateformatter.date(from: dateString)

        guard let timeInterval = newDate?.timeIntervalSince1970 else {
            return nil
        }
        var time = timespec(tv_sec: __darwin_time_t(timeInterval), tv_nsec: 0)
        return DispatchWallTime(timespec: time)
    }
}
複製代碼

下面經過字符串便可建立時間戳:

let time = Date.getWallTime(from: "2018-03-08 13:30:00")
複製代碼

timespec

這是 Darwin 內核中的一個結構體,用於表示一個絕對時間點,它描述的是從格林威治時間 1970年1月1日零點 開始指定時間間隔後的時間點,精確到納秒,結構以下:

struct timespec {
    __darwin_time_t tv_sec; // 表示時間的秒數
    long tv_nsec; // 表示時間的1秒內的部分(至關於小數部分),以納秒爲 1 個單位計數。
};

let time = timespec(tv_sec: __darwin_time_t(86400), tv_nsec: 10) // 表示 1970-1-2 號第 10 納秒
複製代碼

timeval

這是 Darwin 內核中的一個結構體,也用於表示一個絕對時間點,它描述的是從格林威治時間 1970年1月1日零點 開始指定時間間隔後的時間點,精確到微秒,結構以下:

struct timeval {
    __darwin_time_t tv_sec; // 表示時間的秒數
    __darwin_suseconds_t tv_usec; // 表示時間的1秒內的部分(至關於小數部分),以微秒爲 1 個單位計數。
};
複製代碼

gettimeofday

這是 Unix 系統中的一個獲取當前時間的方法,它接收兩個指針參數,執行後將修改指針對應的結構體值,一個參數爲 timeval 類型的時間結構體指針,另外一個爲時區結構體指針(時區在此方法中已再也不使用,設爲 nil 便可)。方法返回 0 時表示獲取成功,返回 -1 時表示獲取失敗:

var getTimeval = timeval() // 原始時間
let time = gettimeofday(&getTimeval, nil) // 再次讀取 getTimeval 即爲當前時間
複製代碼

問答習題

最後留下幾個問題給你們思考。

隊列與任務特性

  1. 主隊列只能使用主線程嗎?
  2. 串行隊列可使用多個線程嗎?若是能夠,能夠同時使用多個線程嗎?
  3. 向主隊列中提交同步任務會致使死鎖嗎?
  4. 向串行隊列中提交同步任務會致使死鎖嗎?
  5. 向並行隊列中提交同步任務會致使死鎖嗎?

擴展閱讀

源碼

官方 GCD Swift 源碼

官方 Operation Swift 源碼 (推薦看一下,更易懂好用的 Operation 類原來封裝起來這麼簡單。)

鳴謝

本教程在撰寫過程當中,參考或從如下文章中得到靈感,感謝如下文章及做者的幫助:



歡迎訪問 個人我的網站 ,閱讀更多文章。


題圖:Mission San Xavier del Bac - Matt Artz @unsplash

相關文章
相關標籤/搜索