本教程將帶你詳細瞭解 GCD 的概念和用法,經過文中的代碼示例和附帶的 Github 示例工程,能夠進一步加深對這些概念的體會。附帶的示例工程是一個完整可運行的 App 項目:DispatchQueueTest,項目地址點此處。本教程提供離線版,閱讀體驗更佳: HTML 版 、PDF 版。html
GCD 全稱是 Grand Central Dispatch
,翻譯過來就是大規模中央調度。根據官方文檔,它的做用是:「經過向系統管理的調度隊列中提交任務,在多核硬件上同時執行代碼。」。它提供了一套機制,讓你能夠充分利用硬件的多核性能,而且讓你不用再調用那些繁瑣的底層線程 API,編寫易於理解和修改的代碼。
node
GCD 的核心就是爲了解決如何讓程序有序、高效的運行,由此衍生出隊列等概念和一系列的方法。爲了弄清楚這些概念,咱們先來看看程序執行存在哪些問題須要解決。git
在 GCD 中把程序執行時作的事情都當成任務,一段代碼、一個 API 調用、一個方法、函數、閉包等,都是任務,一個應用就是由不少任務組成的。任務的執行須要時間和相應的順序,耗時有長短,順序有前後,任務只有按照正確的時間和順序進行編排,應用才能按照你的預期運行。咱們舉音樂播放的例子來看看關於任務有哪些需求。github
以上列舉了 6 個經典的任務執行須要的特性,在 GCD 中分別提供瞭如下方法來支持它們:macos
下面咱們先從隊列開始分析。編程
在系統底層,程序是運行在線程之中的,若是咱們直接在線程層面進行操做,咱們就須要告訴程序它應該運行在哪一個線程、什麼時候開始、什麼時候結束等,這一列的操做都很是繁瑣,並且很容易出錯。爲了簡化線程的操做,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) // 後臺運行級別
複製代碼
有些任務咱們必須等待它的執行結果才能進行下一步,這種執行任務的方式稱爲同步,簡稱同步任務;有些任務只要把它放入隊列就能夠無論它了,能夠繼續執行其餘任務,按這種方式執行的任務,稱爲異步任務。
特性:任務一經提交就會阻塞當前線程(當前線程能夠理解爲下方代碼示例中執行 sync
方法所在的線程 thread0
),並請求隊列當即安排其執行,執行任務的線程 thread1
默認等於 thread0
,即同步任務直接在當前線程運行,任務完成後恢復線程原任務。
任務提交方式以下:
// current thread - thread0
queue.sync {
// current thread - thread1 == thread0
// do something
}
複製代碼
咱們分別根據下圖中的 4 種狀況舉 4 個例子,來講明同步任務的特性。
thread0
,任務完成後再恢復線程 thread0
中被阻塞的任務。看例子前先介紹兩個輔助方法:
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 taskend 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: trueconcurrentQueue 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: trueserialQueue2 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 threadconcurrentQueue 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 threadserialQueue 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 taskserialQueue 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 threadasync task1--->
async task2--->
async task2 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task2async task3--->
async task3 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task3async task1 at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->async task1barrierTask--->
barrierTask at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->barrierTaskasync task5--->
async task5 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task5async task6--->
async task6 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task6async 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]
下面介紹一下在建立隊列時,能夠設置的一些更豐富的屬性。建立隊列的完整方法以下:
convenience init(label: String, qos: DispatchQoS = default, attributes: DispatchQueue.Attributes = default, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = default, target: DispatchQueue? = default)
複製代碼
隊列在執行上是有優先級的,更高的優先級能夠享受更多的計算資源,從高到低包含如下幾個等級:
包含兩個屬性:
queue.activate()
方法。這個屬性表示 autorelease pool
的自動釋放頻率, autorelease pool
管理着任務對象的內存週期。
包含三個屬性:
autorelease pool
,須要手動管理。通常任務採用 .workItem
屬性就夠了,特殊任務如在任務內部大量重複建立對象的操做可選擇 .never
屬性手動建立 autorelease pool
。
這個屬性設置的是一個隊列的目標隊列,即實際將該隊列的任務放入指定隊列中運行。目標隊列最終約束了隊列優先級等屬性。
在程序中手動建立的隊列,其實最後都指向系統自帶的 主隊列
或 全局併發隊列
。
你也許會問,爲何不直接將任務添加至系統隊列中,而是自定義隊列,由於這樣的好處是能夠將任務進行分組管理。如單獨阻塞隊列中的任務,而不是阻塞系統隊列中的所有任務。若是阻塞了目標隊列,全部指向它的原隊列也將被阻塞。
在 Swift 3 及以後,對目標隊列的設置進行了約束,只有兩種狀況能夠顯式地設置目標隊列(緣由參考):
attributes
設定爲 initiallyInactive
,而後在隊列執行 activate()
以前能夠指定目標隊列。在其餘地方都不能再改變目標隊列。
關於目標隊列的詳細闡述,能夠參考這篇文章:GCD Target Queues。
有時候你並不須要當即將任務加入隊列中運行,而是須要等待一段時間後再進入隊列中,這時候可使用 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
方法接收兩個參數,並返回一個閉包:
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
GCD 提供了一套機制,能夠掛起隊列中還沒有執行的任務,已經在執行的任務會繼續執行完,後續還能夠手動再喚醒隊列。
這兩個方法是屬於 DispatchObject
對象的方法,而這個對象是 DispatchQueue
、DispatchGroup
、DispatchSource
、DispatchIO
、DispatchSemaphore
這幾個類的父類,但這兩個方法只有 DispatchQueue
、DispatchSource
支持,調用時需注意。
掛起使用 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
若是再按一次喚醒按鈕,則會提示:
喚醒的次數過多
任務組至關於一系列任務的鬆散集合,它能夠來自相同或不一樣隊列,扮演着組織者的角色。它能夠通知外部隊列,組內的任務是否都已完成。或者阻塞當前的線程,直到組內的任務都完成。全部適合組隊執行的任務均可以使用任務組,且任務組更適合集合異步任務(若是都是同步任務,直接使用串行隊列便可)。
建立的方式至關簡單,無需任何參數:
let queueGroup = DispatchGroup()
複製代碼
有兩種方式加入任務組:
let queue = DispatchQueue.global()
queue.async(group: queueGroup) {
print("喝一杯牛奶")
}
複製代碼
Group.enter()
、 Group.leave()
配對方法,標識任務加入任務組。queueGroup.enter()
queue.async {
print("吃一個蘋果")
queueGroup.leave()
}
複製代碼
兩種加入方式在對任務處理的特性上是沒有區別的,只是便利之處不一樣。若是任務所在的隊列是本身建立或引用的系統隊列,那麼直接使用第一種方式直接加入便可。若是任務是由系統或第三方的 API 建立的,因爲沒法獲取到對應的隊列,只能使用第二種方式將任務加入組內,例如將 URLSession
的 addDataTask
方法加入任務組中:
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
方法中還能夠指定具體的時間,它表示將等待不超過這個時間,若是任務組在指定時間以內完成則當即恢復當前線程,不然將等到時間結束時再恢復當前線程。
DispatchTime
,它表示一個時間間隔,精確到納秒(1/1000,000,000 秒):let waitTime = DispatchTime.now() + 2.0 // 表示從當前時間開始後 2 秒,數字字面量也能夠改成使用 TimeInterval 類型變量
group.wait(timeout: waitTime)
複製代碼
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 中的 URLSession
的 dataTask
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
任務組通知:任務都完成了。書籍 《挪威的森林》的標籤信息以下:
村上春樹
挪威的森林
小說
日本文學
日本
GCD 中提供了一個 DispatchSource
類,它能夠幫你監聽系統底層一些對象的活動,例如這些對象: Mach port
、Unix descriptor
、Unix signal
、VFS node
,並容許你在這些活動發生時,向隊列提交一個任務以進行異步處理。
這些可監聽的對象都有具體的類型,你可使用 DispatchSource
的類方法來構建這些類型,這裏就不一一列舉了。下面以文件監聽爲例說明 DispatchSource
的用法。
例子中監聽了一個指定目錄下文件的寫入事件,建立監聽主要有幾個步驟:
makeFileSystemObjectSource
方法建立 sourcesetEventHandler
設定事件處理程序,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
的更多例子還能夠 參考這裏。
DispatchIO
對象提供一個操做文件描述符的通道。簡單講你能夠利用多線程異步高效地讀寫文件。
發起讀寫操做通常步驟以下:
DispatchIO
對象,或者說建立一個通道,並設置結束處理閉包。read
/ write
方法close
方法關閉通道close
方法後系統將自動調用結束處理閉包下面介紹下各方法的使用。
通常使用兩種方式初始化:文件描述符,或者文件路徑。
文件描述符使用 open
方法建立:open(_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: mode_t) -> Int32
,第一個參數是 UnsafePointer<Int8>
類型的路徑,oflag
、mode
指文件的操做權限,一個是系統 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 | 可執行 |
建立的通道有兩種類型:
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
)。
每讀取到一個數據塊都會調用你設置的處理閉包,系統會提供三個入參給你:結束標誌、本次讀取到的數據塊、錯誤碼:
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 之間的隔離,經過任務組的enter
、leave
、wait
方法能夠作到較好的隔離。
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。
DispatchData
對象能夠管理基於內存的數據緩衝區。這個數據緩衝區對外表現爲連續的內存區域,但內部可能由多個獨立的內存區域組成。
DispatchData
對象不少特性相似於 Data
對象,且 Data
對象能夠轉換爲 DispatchData
對象,而經過 DispatchIO
的 read
方法得到的數據也是封裝爲 DispatchData
對象的。
下面再看個示例,經過 Data
、DispatchData
、DispatchIO
這三種類型結合,完成內存佔用更小也一樣快速的文件讀寫操做。
示例 10.1:將兩個大文件合併爲一個文件(與示例 9.1 相似)。
實現思路:首先將兩個文件轉換爲 Data
對象,再轉換爲 DispatchData
對象,而後拼接兩個對象爲一個 DispatchData
對象,最後經過 DispatchIO
的 write
方法寫入文件中。看起來有屢次的轉換過程,實際上 Data
類型讀取文件時支持虛擬隱射的方式,而 DispatchData
類型更是支持多個數據塊虛擬拼接,也不佔用什麼內存。
實際上徹底使用
Data
類型也能完成文件合併,利用append
、write
方法便可,可是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 左右,很是快速,而全部閥值的內存佔用都不多。因此設置合理的閥值,對性能的改善也是有幫助的。
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.
在隊列和任務組中,任務其實是被封裝爲一個 DispatchWorkItem
對象的。任務封裝最直接的好處就是能夠取消任務。
前面提到的柵欄任務就是經過封裝任務對象實現的。
先看看它的建立,其中 qos
、flags
參數都有默認值,能夠不填:
let workItem = DispatchWorkItem(qos: .default, flags: DispatchWorkItemFlags()) {
// Do something
}
複製代碼
qos
前面提到過了,這裏說一下 DispatchWorkItemFlags
,它有如下幾個靜態屬性(詳細解釋可參考 官方源碼 ):
執行任務時,調用任務項對象的 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
它經過時間間隔的方式來表示一個時間點,初始時間從系統最近一次開機時間開始計算,並且在系統休眠時暫停計時,等系統恢復後繼續計時,精確到納秒(1/1000,000,000 秒)。能夠直接使用 + 運算符設定延時,若是使用變量延時要使用 TimeInterval 類型:
DispatchTime.now() // 表示當前時間與開機時間的間隔
let twoSecondAfter = DispatchTime.now() + 2.1 // 當前時間以後 2.1 秒
複製代碼
它表示一個絕對時間的時間戳,能夠直接使用字面量表示延時,也能夠借用 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)
複製代碼
首先須要作一些擴展:
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")
複製代碼
這是 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 納秒
複製代碼
這是 Darwin
內核中的一個結構體,也用於表示一個絕對時間點,它描述的是從格林威治時間 1970年1月1日零點
開始指定時間間隔後的時間點,精確到微秒,結構以下:
struct timeval {
__darwin_time_t tv_sec; // 表示時間的秒數
__darwin_suseconds_t tv_usec; // 表示時間的1秒內的部分(至關於小數部分),以微秒爲 1 個單位計數。
};
複製代碼
這是 Unix
系統中的一個獲取當前時間的方法,它接收兩個指針參數,執行後將修改指針對應的結構體值,一個參數爲 timeval
類型的時間結構體指針,另外一個爲時區結構體指針(時區在此方法中已再也不使用,設爲 nil 便可)。方法返回 0 時表示獲取成功,返回 -1 時表示獲取失敗:
var getTimeval = timeval() // 原始時間
let time = gettimeofday(&getTimeval, nil) // 再次讀取 getTimeval 即爲當前時間
複製代碼
最後留下幾個問題給你們思考。
官方 Operation Swift 源碼 (推薦看一下,更易懂好用的 Operation 類原來封裝起來這麼簡單。)
本教程在撰寫過程當中,參考或從如下文章中得到靈感,感謝如下文章及做者的幫助:
歡迎訪問 個人我的網站 ,閱讀更多文章。