Dispatch Queue任務執(zhí)行與Dispatch Source
大?。?/span>0.5 MB 人氣: 2017-10-11 需要積分:1
標簽:
導讀本文為讀《Concurrency Programming Guide》筆記第三篇,在對OS X和iOS應用開發(fā)中實現(xiàn)任務異步執(zhí)行的技術、注意事項、Operation與Dispatch Queues實踐解析后,作者付宇軒(@DevTalking)著重分享了讓Dispatch Queue執(zhí)行任務的那些事兒。當然,本著“Talk is cheap, show me the code”原則,除卻講解外,作者還分享了各個知識點具體操作實現(xiàn)的Swift代碼片段。系列閱讀(文章點擊【閱讀原文】)
iOS開發(fā)中設計并發(fā)任務技術與注意事項
iOS并發(fā)編程中Operation與Dispatch Queues實踐
iOS并發(fā)編程指南:Dispatch Queue任務執(zhí)行與Dispatch Source
通過Dispatch Queue執(zhí)行任務
如果想讓Dispatch Queue執(zhí)行任務,首先就是得將任務放入隊列中,我們可以異步的將任務加入隊列,也可以同步的將任務加入隊列,可以一個任務一個任務的加,也可以一組一組的加。這節(jié)我們就來看看將任務加入隊列的那些事。
向隊列添加任務
我們可以使用dispatch_async或者dispatch_async_f函數(shù)異步的向隊列中添加任務,也就是說當我們添加完任務后該函數(shù)會立即返回,我們不需要等待任務執(zhí)行完成,而且我們也不會知道隊列到底何時開始執(zhí)行任務。dispatch_async函數(shù)有兩個參數(shù),一個是目標隊列,類型為dispatch_queue_t,另一個是閉包,類型為dispatch_block_t:
let serialQueue = dispatch_queue_create(“com.example.MySerialQueue”, nil)
dispatch_async(serialQueue, {
print(“Task in the queue.。.”)
})
dispatch_async_f函數(shù)有三個參數(shù),第一個是類型為dispatch_queue_t的目標隊列,第二個是隊列上下文指針,第三個是類型為dispatch_function_t的任務函數(shù),隊列上下文指針為該函數(shù)的唯一參數(shù):
class AddTaskToQueue {
func launch() {
let serialQueue = dispatch_queue_create(“com.example.MySerialQueue”, nil)
dispatch_async(serialQueue, {
print(“Task in the queue.。.”)
})
dispatch_async_f(serialQueue, unsafeBitCast(0, UnsafeMutablePointer《Int》.self), taskFunction())
sleep(3)
}
func taskFunction() -》 dispatch_function_t {
return { context in
print(“Do some work with context.。.”)
}
}
}
let addTaskToQueue = AddTaskToQueue()
addTaskToQueue.launch()
除了這兩個函數(shù),我們還可以使用dispatch_sync和dispatch_sync_f函數(shù)同步的向隊列中添加任務,并且我們要等待直到任務執(zhí)行完成。這兩個函數(shù)和上面的異步添加任務函數(shù)用法完全一致。
那么什么時候用異步什么時候用同步呢,大多數(shù)情況下我們都是在主線程中使用GCD分派任務,為了避免阻塞主線程,影響用戶體驗,所以通常情況下我們都使用異步添加任務的方式。當然為了避免任務與主線程中產(chǎn)生資源競爭的問題,有時候酌情也會使用同步添加任務的方式。
Dispatch Queue的Completion Block
還記得NSOperation的completionBlock屬性嗎,這個回調(diào)函數(shù)在任務執(zhí)行完成后調(diào)用,用于處理有些后續(xù)工作或者消息通知。在Dispatch Queue中并沒有類似的屬性,但是我們可以通過其他方式來實現(xiàn)。舉一個很常見的應用場景,我們在主線程中分派一個下載圖片的任務,讓其在二級線程中執(zhí)行,當圖片下載完成后通知主線程,并由主線程將圖片顯示出來,我們看看簡單的代碼片段:
class DownloadImage {
func dispatchTaskInMainThread() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), downloadImage())
}
func downloadImage() -》 (() -》 ()) {
return {
print(“Downloading image in \(NSThread.currentThread())”)
dispatch_async(dispatch_get_main_queue()) {
print(“Handle image and display in \(NSThread.currentThread())”)
}
}
}
}
我們來看看上面代碼都做了些什么,首先在dispatchTaskInMainThread方法中,我們使用dispatch_get_global_queue函數(shù)獲取到全局并發(fā)隊列,然后將downloadImage下載圖片的方法作為任務添加到該全局隊列中。在downloadImage方法里,當圖片下載完成后通過dispatch_get_main_queue函數(shù)獲取到主隊列,也就是在主線程中對圖片進行處理,這樣我們就達到了Completion Block的效果。
在隊列中循環(huán)執(zhí)行任務
在我們的日常開發(fā)中,經(jīng)常會使用到for循環(huán)來處理一些任務,而且這些任務之間也并沒有先后順序的關聯(lián),每個任務相對比較獨立。遇到這種情況,我們可以用dispatch_apply或dispatch_apply_f函數(shù)讓任務在隊列中循環(huán)執(zhí)行,并且可以是并發(fā)執(zhí)行,這樣相比for循環(huán)的串行執(zhí)行要更加效率:
// for循環(huán)
let arr = [“Swift”, “Objective-C”, “Java”, “Delphi”, “C++”]
for element in arr {
print(“Handle element. the element is \(element)”)
}
// dispatch_apply
dispatch_apply(arr.count, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){ index in
print(“Handle element. the element is \(arr[index])。 Current thread is \(NSThread.currentThread())”)
}
從上面示例代碼片段中可以看到,dispatch_apply函數(shù)有三個參數(shù),第一個參數(shù)是循環(huán)次數(shù),第二個參數(shù)是目標隊列,第三個則是要執(zhí)行的閉包任務,循環(huán)次數(shù)是該閉包的唯一參數(shù)。
暫停和重啟隊列
在Dispatch Queue執(zhí)行任務時,如果我們想暫停隊列,可以使用dispatch_suspend函數(shù),重新讓隊列執(zhí)行任務可以使用dispatch_resume。這里要注意的是暫停隊列只是讓隊列暫時停止執(zhí)行下一個任務,而不是中斷當前正在執(zhí)行的任務。
Dispatch Group的使用
在實際開發(fā)中,為了提升性能我們或許會經(jīng)常使用dispatch_async異步的將任務添加進隊列去執(zhí)行,但有些時候需要之前隊列中的多個任務都執(zhí)行完成之后,才能獲取到正確的或者說想要的結(jié)果供后續(xù)邏輯代碼使用,遇到這種情況,就可以使用Dispatch Group,將多個任務在隊列中歸為一個組,并可以使用dispatch_group_wait函數(shù)讓之后的邏輯代碼等待,直到該組的任務都執(zhí)行完成后再執(zhí)行。
var count = 0
let concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(concurrentQueue) {
print(“Task1 in dispatchGroup.。.”)
sleep(2)
count += 1
}
dispatch_async(concurrentQueue) {
print(“Task2 in dispatchGroup.。.”)
sleep(3)
count += 1
}
dispatch_async(concurrentQueue) {
print(“Task3 in dispatchGroup.。.”)
sleep(1)
count += 1
}
print(“I expect the count is 3, and the factual count is \(count)”)
上面的代碼片段就是我剛才描述的場景,因為使用的是并發(fā)隊列,也不好在每個任務里進行回調(diào)處理,所以我們永遠不會得到正確的count。如果我們使用Dispatch Group事情就簡單多了:
var count = 0
let concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let dispatchGroup = dispatch_group_create()
dispatch_group_async(dispatchGroup, concurrentQueue) {
print(“Task1 in dispatchGroup.。.”)
sleep(2)
count += 1
}
dispatch_group_async(dispatchGroup, concurrentQueue) {
print(“Task2 in dispatchGroup.。.”)
sleep(3)
count += 1
}
dispatch_group_async(dispatchGroup, concurrentQueue) {
print(“Task3 in dispatchGroup.。.”)
sleep(1)
count += 1
}
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
print(“I expect the count is 3, and the factual count is \(count)”)
上面的代碼中,先使用dispatch_group_create函數(shù)創(chuàng)建Dispatch Group,然后使用dispatch_group_async函數(shù)將任務分進組里,然后再添加進隊列中。該函數(shù)有三個參數(shù),分別是Dispatch Group、Dispatch Queue和要執(zhí)行任務的閉包。當添加完任務后使用dispatch_group_wait函數(shù)等待,直到指定組的任務全部完成,才會繼續(xù)執(zhí)行后面的打印語句,該函數(shù)有兩個參數(shù),第一個是目標組,第二個是等待時間DISPATCH_TIME_NOW或DISPATCH_TIME_FOREVER。
Dispatch Source
前面的文章中介紹過Dispatch Source:
Dispatch Source是GCD中的一個基本類型,從字面意思可稱為調(diào)度源,它的作用是當有一些特定的較底層的系統(tǒng)事件發(fā)生時,調(diào)度源會捕捉到這些事件,然后可以做其他的邏輯處理,調(diào)度源有多種類型,分別監(jiān)聽對應類型的系統(tǒng)事件。我們來看看它都有哪些類型:
Timer Dispatch Source:定時調(diào)度源。
Signal Dispatch Source:監(jiān)聽UNIX信號調(diào)度源,比如監(jiān)聽代表掛起指令的SIGSTOP信號。
Deor Dispatch Source:監(jiān)聽文件相關操作和Socket相關操作的調(diào)度源。
Process Dispatch Source:監(jiān)聽進程相關狀態(tài)的調(diào)度源。
Mach port Dispatch Source:監(jiān)聽Mach相關事件的調(diào)度源。
Custom Dispatch Source:監(jiān)聽自定義事件的調(diào)度源。
這一節(jié)就來看看如何使用Dispatch Source。
用通俗一點的話說就是用GCD的函數(shù)指定一個希望監(jiān)聽的系統(tǒng)事件類型,再指定一個捕獲到事件后進行邏輯處理的閉包或者函數(shù)作為回調(diào)函數(shù),然后再指定一個該回調(diào)函數(shù)執(zhí)行的Dispatch Queue即可,當監(jiān)聽到指定的系統(tǒng)事件發(fā)生時會調(diào)用回調(diào)函數(shù),將該回調(diào)函數(shù)作為一個任務放入指定的隊列中執(zhí)行。也就是說當監(jiān)聽到系統(tǒng)事件后就會觸發(fā)一個任務,并自動將其加入隊列執(zhí)行,這里與之前手動添加任務的模式不同,一旦將Diaptach Source與Dispatch Queue關聯(lián)后,只要監(jiān)聽到系統(tǒng)事件,Dispatch Source就會自動將任務(回調(diào)函數(shù))添加到關聯(lián)的隊列中。
有些時候回調(diào)函數(shù)執(zhí)行的時間較長,在這段時間內(nèi)Dispatch Source又監(jiān)聽到多個系統(tǒng)事件,理論上就會形成事件積壓,但好在Dispatch Source有很好的機制解決這個問題,當有多個事件積壓時會根據(jù)事件類型,將它們進行關聯(lián)和結(jié)合,形成一個新的事件。
監(jiān)聽事件類型
Dispatch Source一共可以監(jiān)聽六類事件,分為11個類型,我們來看看都是什么:
DISPATCH_SOURCE_TYPE_DATA_ADD:屬于自定義事件,可以通過dispatch_source_get_data函數(shù)獲取事件變量數(shù)據(jù),在我們自定義的方法中可以調(diào)用dispatch_source_merge_data函數(shù)向Dispatch Source設置數(shù)據(jù),下文中會有詳細的演示。
DISPATCH_SOURCE_TYPE_DATA_OR:屬于自定義事件,用法同上面的類型一樣。
DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口發(fā)送事件。
DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
DISPATCH_SOURCE_TYPE_PROC:與進程相關的事件。
DISPATCH_SOURCE_TYPE_READ:讀文件事件。
DISPATCH_SOURCE_TYPE_WRITE:寫文件事件。
DISPATCH_SOURCE_TYPE_VNODE:文件屬性更改事件。
DISPATCH_SOURCE_TYPE_SIGNAL:接收信號事件。
DISPATCH_SOURCE_TYPE_TIMER:定時器事件。
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:內(nèi)存壓力事件。
創(chuàng)建Dispatch Source
我們可以使用dispatch_source_create函數(shù)創(chuàng)建Dispatch Source,該函數(shù)有四個參數(shù):
type:第一個參數(shù)用于標識Dispatch Source要監(jiān)聽的事件類型,共有11個類型。
handle:第二個參數(shù)是取決于要監(jiān)聽的事件類型,比如如果是監(jiān)聽Mach端口相關的事件,那么該參數(shù)就是mach_port_t類型的Mach端口號,如果是監(jiān)聽事件變量數(shù)據(jù)類型的事件那么該參數(shù)就不需要,設置為0就可以了。
mask:第三個參數(shù)同樣取決于要監(jiān)聽的事件類型,比如如果是監(jiān)聽文件屬性更改的事件,那么該參數(shù)就標識文件的哪個屬性,比如DISPATCH_VNODE_RENAME。
queue:第四個參數(shù)設置回調(diào)函數(shù)所在的隊列。
let dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatchQueue)
上面的代碼就是創(chuàng)建Dispatch Source的簡單示例。
設置事件處理器
前文中提到過,當Dispatch Source監(jiān)聽到事件時會調(diào)用指定的回調(diào)函數(shù)或閉包,該回調(diào)函數(shù)或閉包就是Dispatch Source的事件處理器。我們可以使用dispatch_source_set_event_handler或dispatch_source_set_event_handler_f函數(shù)給創(chuàng)建好的Dispatch Source設置處理器,前者是設置閉包形式的處理器,后者是設置函數(shù)形式的處理器:
dispatch_source_set_event_handler(dispatchSource, {
print(“Dispatch Source 事件處理器。..”)
})
// 根據(jù)閉包尾隨的特性,還可以有下面的寫法
dispatch_source_set_event_handler(dispatchSource) {
print(“Dispatch Source 事件處理器。..”)
}
從上面示例代碼中可以看到,該函數(shù)有兩個參數(shù),第一個是設置目標Dispatch Source,第二個參數(shù)就是設置處理器了。
既然是事件處理器,那么肯定需要獲取一些Dispatch Source的信息,GCD提供了三個在處理器中獲取Dispatch Source相關信息的函數(shù),比如handle、mask。而且針對不同類型的Dispatch Source,這三個函數(shù)返回數(shù)據(jù)的值和類型都會不一樣,下面來看看這三個函數(shù):
dispatch_source_get_handle:這個函數(shù)用于獲取在創(chuàng)建Dispatch Source時設置的第二個參數(shù)handle。
如果是讀寫文件的Dispatch Source,返回的就是描述符。
如果是信號類型的Dispatch Source,返回的是int類型的信號數(shù)。
如果是進程類型的Dispatch Source,返回的是pid_t類型的進程id。
如果是Mach端口類型的Dispatch Source,返回的是mach_port_t類型的Mach端口。
dispatch_source_get_data:該函數(shù)用于獲取Dispatch Source監(jiān)聽到事件的相關數(shù)據(jù)。
如果是讀文件類型的Dispatch Source,返回的是讀到文件內(nèi)容的字節(jié)數(shù)。
如果是寫文件類型的Dispatch Source,返回的是文件是否可寫的標識符,正數(shù)表示可寫,負數(shù)表示不可寫。
如果是監(jiān)聽文件屬性更改類型的Dispatch Source,返回的是監(jiān)聽到的有更改的文件屬性,用常量表示,比如DISPATCH_VNODE_RENAME等。
如果是進程類型的Dispatch Source,返回監(jiān)聽到的進程狀態(tài),用常量表示,比如DISPATCH_PROC_EXIT等。
如果是Mach端口類型的Dispatch Source,返回Mach端口的狀態(tài),用常量表示,比如DISPATCH_MACH_SEND_DEAD等。
如果是自定義事件類型的Dispatch Source,返回使用dispatch_source_merge_data函數(shù)設置的數(shù)據(jù)。
dispatch_source_get_mask:該函數(shù)用于獲取在創(chuàng)建Dispatch Source時設置的第三個參數(shù)mask。在進程類型,文件屬性更改類型,Mach端口類型的Dispatch Source下該函數(shù)返回的結(jié)果與dispatch_source_get_data一樣。
注冊Cancellation Handler
Cancellation Handler就是當Dispatch Source被釋放時用來處理一些后續(xù)事情,比如關閉文件描述符或者釋放Mach端口等。我們可以使用dispatch_source_set_cancel_handler函數(shù)或者dispatch_source_set_cancel_handler_f函數(shù)給Dispatch Source注冊Cancellation Handler:
dispatch_source_set_cancel_handler(dispatchSource) {
print(“進行善后處理。..”)
}
該函數(shù)有兩個參數(shù),第一個參數(shù)是目標Dispatch Source,第二個參數(shù)就是要進行善后處理的閉包或者函數(shù)。
更改Dispatch Source的目標隊列
在上文中,我們說過可以使用dispatch_source_create函數(shù)創(chuàng)建Dispatch Source,并且在創(chuàng)建時會指定回調(diào)函數(shù)執(zhí)行的隊列,那么如果事后想更改隊列,比如說想更改隊列的優(yōu)先級,這時我們可以使用dispatch_set_target_queue函數(shù)實現(xiàn):
let dispatchQueueDefaultPriority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatchQueueDefaultPriority)
let dispatchQueueLowPriority = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
dispatch_set_target_queue(dispatchSource, dispatchQueueLowPriority)
這里需要注意的是,如果在更改目標隊列時,Dispatch Source已經(jīng)監(jiān)聽到相關事件,并且回調(diào)函數(shù)已經(jīng)在之前的隊列中執(zhí)行了,那么會一直在舊的隊列中執(zhí)行完成,不會轉(zhuǎn)移到新的隊列中去。
暫停恢復Dispatch Source
暫停和恢復Dispatch Source與Dispatch Queue一樣,都適用dispatch_suspend和dispatch_resume函數(shù)。這里需要注意的是剛創(chuàng)建好的Dispatch Source是處于暫停狀態(tài)的,所以使用時需要用dispatch_resume函數(shù)將其啟動。
廢除Dispatch Source
如果我們不再需要使用某個Dispatch Source時,可以使用dispatch_source_cancel函數(shù)廢除,該函數(shù)只有一個參數(shù),那就是目標Dispatch Source。
Dispatch Source實踐
說了這么多,這一節(jié)來看看Dispatch Source到底怎么用。
用Dispatch Source監(jiān)聽定時器
Dispatch Source能監(jiān)聽的事件中有一個類型就是定時器,我們來看看如何實現(xiàn):
class TestDispatchSource {
func launch() {
let dispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let timer = createTimerDispatchSource(dispatch_time(DISPATCH_TIME_NOW, 0), interval: NSEC_PER_SEC * 5, leeway: 0, queue: dispatchQueue) {
print(“處理定時任務,該任務每5秒執(zhí)行一次。..”)
}
dispatch_resume(timer)
sleep(30)
}
func createTimerDispatchSource(startTime: dispatch_time_t, interval: UInt64, leeway: UInt64, queue: dispatch_queue_t, handler: dispatch_block_t) -》 dispatch_source_t {
let timerDispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
dispatch_source_set_timer(timerDispatchSource, startTime, interval, leeway)
dispatch_source_set_event_handler(timerDispatchSource, handler)
return timerDispatchSource
}
}
上面的代碼示例中一個新的函數(shù)dispatch_source_set_timer,該函數(shù)的作用就是給監(jiān)聽事件類型為DISPATCH_SOURCE_TYPE_TIMER的Dispatch Source設置相關屬性,該函數(shù)有四個參數(shù):
source:該參數(shù)為目標Dispatch Source,類型為dispatch_source_t.
start:該參數(shù)為定時器的起始時間,類型為dispatch_time_t。
interval:該參數(shù)為定時器的間隔時間,類型為UInt64,間隔時間的單位是納秒。
leeway:該參數(shù)為間隔時間的精度,類型為UInt64,時間單位也是納秒。
用Dispatch Source監(jiān)聽自定義事件
Dispatch Source能監(jiān)聽的事件中有一個類型是自定義事件,下面我們來看看如何使用:
class TestDispatchSource {
func launch() {
var totalProcess = 0
let dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue())
dispatch_source_set_event_handler(dispatchSource) {
let process = dispatch_source_get_data(dispatchSource)
totalProcess += Int(process)
print(“這里可以在主線程更新UI,顯示進度條。..進度為\(totalProcess)%”)
}
dispatch_resume(dispatchSource)
generateCustomEvent(dispatchSource)
}
func generateCustomEvent(dispatchSource: dispatch_source_t) {
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
for index in 0.。.100 {
dispatch_sync(queue) {
print(“模擬自定義事件。..進度為\(index)%”)
dispatch_source_merge_data(dispatchSource, 1)
sleep(2)
}
}
}
}
我們來看看generateCustomEvent(dispatchSource: dispatch_source_t)方法,該方法的作用的是模擬自定義事件,首先創(chuàng)建一個全局并發(fā)隊列,然后循環(huán)讓其執(zhí)行任務,在執(zhí)行的任務里調(diào)用dispatch_source_merge_data函數(shù),就可以觸發(fā)監(jiān)聽類型為DISPATCH_SOURCE_TYPE_DATA_ADD或者DISPATCH_SOURCE_TYPE_DATA_OR的Dispatch Source。該函數(shù)有兩個參數(shù),第一個參數(shù)是目標Dispatch Source,第二個參數(shù)的類型是無符號長整型,用于向目標Dispatch Source中的對應變量追加指定的數(shù)。
我們再來看看如何監(jiān)聽自定義時間,首先創(chuàng)建類型為DISPATCH_SOURCE_TYPE_DATA_ADD的Dispatch Source,然后設置回調(diào)閉包,在閉包中使用dispatch_source_get_data獲取追加的變量值,該函數(shù)只有一個參數(shù),就是目標Dispatch Source,這里需要注意的是通過dispatch_source_get_data函數(shù)獲取的變量值并不是累加值,而是每次調(diào)用dispatch_source_merge_data函數(shù)時設置的值,所以在上面的示例中用totalProcess變量累加每次獲取到的值。
上面的示例可以用來模擬后臺進行下載,根據(jù)下載的數(shù)據(jù)量使用dispatch_source_merge_data函數(shù)給目標Dispatch Source設置相應的變量值,然后在主線程中監(jiān)聽到Dispatch Source的自定義事件,通過dispatch_source_get_data函數(shù)獲取到變量,用于更新顯示進度條的UI。
?
非常好我支持^.^
(0) 0%
不好我反對
(0) 0%