GCD介紹(三):Dispatch Sources
何為Dispatch Sources
簡(jiǎn)單來(lái)說(shuō),dispatch source是一個(gè)監(jiān)視某些類(lèi)型事件的對(duì)象。當(dāng)這些事件發(fā)生時(shí),它自動(dòng)將一個(gè)block放入一個(gè)dispatch queue的執(zhí)行例程中。
說(shuō)的貌似有點(diǎn)不清不楚。我們到底討論哪些事件類(lèi)型?
下面是GCD 10.6.0版本支持的事件:
- Mach port send right state changes.
- Mach port receive right state changes.
- External process state change.
- File descriptor ready for read.
- File descriptor ready for write.
- Filesystem node event.
- POSIX signal.
- Custom timer.
- Custom event.
這是一堆很有用的東西,它支持所有kqueue所支持的事件(kqueue是什么?見(jiàn)http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什么?見(jiàn)http://en.wikipedia.org/wiki/Mach_(kernel))端口、內(nèi)建計(jì)時(shí)器支持(這樣我們就不用使用超時(shí)參數(shù)來(lái)創(chuàng)建自己的計(jì)時(shí)器)和用戶(hù)事件。
用戶(hù)事件
這些事件里面多數(shù)都可以從名字中看出含義,但是你可能想知道啥叫用戶(hù)事件。簡(jiǎn)單地說(shuō),這種事件是由你調(diào)用dispatch_source_merge_data函數(shù)來(lái)向自己發(fā)出的信號(hào)。
這個(gè)名字對(duì)于一個(gè)發(fā)出事件信號(hào)的函數(shù)來(lái)說(shuō),太怪異了。這個(gè)名字的來(lái)由是GCD會(huì)在事件句柄被執(zhí)行之前自動(dòng)將多個(gè)事件進(jìn)行聯(lián)結(jié)。你可以將數(shù)據(jù)“拼接”至dispatch source中任意次,并且如果dispatch queue在這期間繁忙的話,GCD只會(huì)調(diào)用該句柄一次(不要覺(jué)得這樣會(huì)有問(wèn)題,看完下面的內(nèi)容你就明白了)。
用戶(hù)事件有兩種: DISPATCH_SOURCE_TYPE_DATA_ADD
和 DISPATCH_SOURCE_TYPE_DATA_OR
.用戶(hù)事件源有個(gè) unsigned long data
屬性,我們將一個(gè) unsigned long
傳入 dispatch_source_merge_data
。當(dāng)使用 _ADD
版本時(shí),事件在聯(lián)結(jié)時(shí)會(huì)把這些數(shù)字相加。當(dāng)使用 _OR
版本時(shí),事件在聯(lián)結(jié)時(shí)會(huì)把這些數(shù)字邏輯與運(yùn)算。當(dāng)事件句柄執(zhí)行時(shí),我們可以使用dispatch_source_get_data函數(shù)訪問(wèn)當(dāng)前值,然后這個(gè)值會(huì)被重置為0。
讓我假設(shè)一種情況。假設(shè)一些異步執(zhí)行的代碼會(huì)更新一個(gè)進(jìn)度條。因?yàn)橹骶€程只不過(guò)是GCD的另一個(gè)dispatch queue而已,所以我們可以將GUI更新工作push到主線程中。然而,這些事件可能會(huì)有一大堆,我們不想對(duì)GUI進(jìn)行頻繁而累贅的更新,理想的情況是當(dāng)主線程繁忙時(shí)將所有的改變聯(lián)結(jié)起來(lái)。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我們可以將工作拼接起來(lái),然后主線程可以知道從上一次處理完事件到現(xiàn)在一共發(fā)生了多少改變,然后將這一整段改變一次更新至進(jìn)度條。
啥也不說(shuō)了,上代碼:
- dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
- dispatch_source_set_event_handler(source, ^{
- [progressIndicator incrementBy:dispatch_source_get_data(source)];
- });
- dispatch_resume(source);
- dispatch_apply([array count], globalQueue, ^(size_t index) {
- // do some work on data at index
- dispatch_source_merge_data(source, 1);
- });
(對(duì)于這段代碼,我很想說(shuō)點(diǎn)什么,我第一次用dispatch source時(shí),我糾結(jié)了很久很久,真讓人蛋疼:Dispatch source啟動(dòng)時(shí)默認(rèn)狀態(tài)是掛起的,我們創(chuàng)建完畢之后得主動(dòng)恢復(fù),否則事件不會(huì)被傳遞,也不會(huì)被執(zhí)行)
假設(shè)你已經(jīng)將進(jìn)度條的min/max值設(shè)置好了,那么這段代碼就完美了。數(shù)據(jù)會(huì)被并發(fā)處理。當(dāng)每一段數(shù)據(jù)完成后,會(huì)通知dispatch source并將dispatch source data加1,這樣我們就認(rèn)為一個(gè)單元的工作完成了。事件句柄根據(jù)已完成的工作單元來(lái)更新進(jìn)度條。若主線程比較空閑并且這些工作單元進(jìn)行的比較慢,那么事件句柄會(huì)在每個(gè)工作單元完成的時(shí)候被調(diào)用,實(shí)時(shí)更新。如果主線程忙于其他工作,或者工作單元完成速度很快,那么完成事件會(huì)被聯(lián)結(jié)起來(lái),導(dǎo)致進(jìn)度條只在主線程變得可用時(shí)才被更新,并且一次將積累的改變更新至GUI。
現(xiàn)在你可能會(huì)想,聽(tīng)起來(lái)倒是不錯(cuò),但是要是我不想讓事件被聯(lián)結(jié)呢?有時(shí)候你可能想讓每一次信號(hào)都會(huì)引起響應(yīng),什么后臺(tái)的智能玩意兒統(tǒng)統(tǒng)不要。啊。。其實(shí)很簡(jiǎn)單的,別把自己繞進(jìn)去了。如果你想讓每一個(gè)信號(hào)都得到響應(yīng),那使用dispatch_async函數(shù)不就行了。實(shí)際上,使用的dispatch source而不使用dispatch_async的唯一原因就是利用聯(lián)結(jié)的優(yōu)勢(shì)。
內(nèi)建事件
上面就是怎樣使用用戶(hù)事件,那么內(nèi)建事件呢?看看下面這個(gè)例子,用GCD讀取標(biāo)準(zhǔn)輸入:
- dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_source_t stdinSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
- STDIN_FILENO,
- 0,
- globalQueue);
- dispatch_source_set_event_handler(stdinSource, ^{
- char buf[1024];
- int len = read(STDIN_FILENO, buf, sizeof(buf));
- if(len > 0)
- NSLog(@"Got data from stdin: %.*s", len, buf);
- });
- dispatch_resume(stdinSource);
簡(jiǎn)單的要死!因?yàn)槲覀兪褂玫氖侨株?duì)列,句柄自動(dòng)在后臺(tái)執(zhí)行,與程序的其他部分并行,這意味著對(duì)這種情況的提速:事件進(jìn)入程序時(shí),程序正在處理其他事務(wù)。
這是標(biāo)準(zhǔn)的UNIX方式來(lái)處理事務(wù)的好處,不用去寫(xiě)loop。如果使用經(jīng)典的 read
調(diào)用,我們還得萬(wàn)分留神,因?yàn)榉祷氐臄?shù)據(jù)可能比請(qǐng)求的少,還得忍受無(wú)厘頭的“errors”,比如 EINTR
(系統(tǒng)調(diào)用中斷)。使用GCD,我們啥都不用管,就從這些蛋疼的情況里解脫了。如果我們?cè)谖募枋龇辛粝铝宋醋x取的數(shù)據(jù),GCD會(huì)再次調(diào)用我們的句柄。
對(duì)于標(biāo)準(zhǔn)輸入,這沒(méi)什么問(wèn)題,但是對(duì)于其他文件描述符,我們必須考慮在完成讀寫(xiě)之后怎樣清除描述符。對(duì)于dispatch source還處于活躍狀態(tài)時(shí),我們決不能關(guān)閉描述符。如果另一個(gè)文件描述符被創(chuàng)建了(可能是另一個(gè)線程創(chuàng)建的)并且新的描述符剛好被分配了相同的數(shù)字,那么你的dispatch source可能會(huì)在不應(yīng)該的時(shí)候突然進(jìn)入讀寫(xiě)狀態(tài)。de這個(gè)bug可不是什么好玩的事兒。
適當(dāng)?shù)那宄绞绞鞘褂?nbsp;dispatch_source_set_cancel_handler
,并傳入一個(gè)block來(lái)關(guān)閉文件描述符。然后我們使用 dispatch_source_cancel
來(lái)取消dispatch source,使得句柄被調(diào)用,然后文件描述符被關(guān)閉。
使用其他dispatch source類(lèi)型也差不多??偟膩?lái)說(shuō),你提供一個(gè)source(mach port、文件描述符、進(jìn)程ID等等)的區(qū)分符來(lái)作為diapatch source的句柄。mask參數(shù)通常不會(huì)被使用,但是對(duì)于 DISPATCH_SOURCE_TYPE_PROC
來(lái)說(shuō)mask指的是我們想要接受哪一種進(jìn)程事件。然后我們提供一個(gè)句柄,然后恢復(fù)這個(gè)source(前面我加粗字體所說(shuō)的,得先恢復(fù)),搞定。dispatch source也提供一個(gè)特定于source的data,我們使用 dispatch_source_get_data
函數(shù)來(lái)訪問(wèn)它。例如,文件描述符會(huì)給出大致可用的字節(jié)數(shù)。進(jìn)程source會(huì)給出上次調(diào)用之后發(fā)生的事件的mask。具體每種source給出的data的含義,看man page吧。
計(jì)時(shí)器
計(jì)時(shí)器事件稍有不同。它們不使用handle/mask參數(shù),計(jì)時(shí)器事件使用另外一個(gè)函數(shù) dispatch_source_set_timer
來(lái)配置計(jì)時(shí)器。這個(gè)函數(shù)使用三個(gè)參數(shù)來(lái)控制計(jì)時(shí)器觸發(fā):
start
參數(shù)控制計(jì)時(shí)器第一次觸發(fā)的時(shí)刻。參數(shù)類(lèi)型是 dispatch_time_t
,這是一個(gè)opaque類(lèi)型,我們不能直接操作它。我們得需要 dispatch_time
和 dispatch_walltime
函數(shù)來(lái)創(chuàng)建它們。另外,常量 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
通常很有用。
interval
參數(shù)沒(méi)什么好解釋的。
leeway
參數(shù)比較有意思。這個(gè)參數(shù)告訴系統(tǒng)我們需要計(jì)時(shí)器觸發(fā)的精準(zhǔn)程度。所有的計(jì)時(shí)器都不會(huì)保證100%精準(zhǔn),這個(gè)參數(shù)用來(lái)告訴系統(tǒng)你希望系統(tǒng)保證精準(zhǔn)的努力程度。如果你希望一個(gè)計(jì)時(shí)器沒(méi)五秒觸發(fā)一次,并且越準(zhǔn)越好,那么你傳遞0為參數(shù)。另外,如果是一個(gè)周期性任務(wù),比如檢查email,那么你會(huì)希望每十分鐘檢查一次,但是不用那么精準(zhǔn)。所以你可以傳入60,告訴系統(tǒng)60秒的誤差是可接受的。
這樣有什么意義呢?簡(jiǎn)單來(lái)說(shuō),就是降低資源消耗。如果系統(tǒng)可以讓cpu休息足夠長(zhǎng)的時(shí)間,并在每次醒來(lái)的時(shí)候執(zhí)行一個(gè)任務(wù)集合,而不是不斷的醒來(lái)睡去以執(zhí)行任務(wù),那么系統(tǒng)會(huì)更高效。如果傳入一個(gè)比較大的leeway給你的計(jì)時(shí)器,意味著你允許系統(tǒng)拖延你的計(jì)時(shí)器來(lái)將計(jì)時(shí)器任務(wù)與其他任務(wù)聯(lián)合起來(lái)一起執(zhí)行。
總結(jié)
現(xiàn)在你知道怎樣使用GCD的dispatch source功能來(lái)監(jiān)視文件描述符、計(jì)時(shí)器、聯(lián)結(jié)的用戶(hù)事件以及其他類(lèi)似的行為。由于dispatch source完全與dispatch queue相集成,所以你可以使用任意的dispatch queue。你可以將一個(gè)dispatch source的句柄在主線程中執(zhí)行、在全局隊(duì)列中并發(fā)執(zhí)行、或者在用戶(hù)隊(duì)列中串行執(zhí)行(執(zhí)行時(shí)會(huì)將程序的其他模塊的運(yùn)算考慮在內(nèi))。
下一篇我會(huì)討論如何對(duì)dispatch queue進(jìn)行掛起、恢復(fù)、重定目標(biāo)操作;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能。