何爲Dispatch Sources
簡單來說,dispatch source是一個監視某些類型事件的對象。當這些事件發生時,它自動將一個block放入一個dispatch queue的執行例程中。
說的貌似有點不清不楚。我們到底討論哪些事件類型?
下面是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是什麼?見http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什麼?見http://en.wikipedia.org/wiki/Mach_(kernel))端口、內建計時器支持(這樣我們就不用使用超時參數來創建自己的計時器)和用戶事件。
用戶事件
這些事件裏面多數都可以從名字中看出含義,但是你可能想知道啥叫用戶事件。簡單地說,這種事件是由你調用dispatch_source_merge_data函數來向自己發出的信號。
這個名字對於一個發出事件信號的函數來說,太怪異了。這個名字的來由是GCD會在事件句柄被執行之前自動將多個事件進行聯結。你可以將數據“拼接”至dispatch source中任意次,並且如果dispatch queue在這期間繁忙的話,GCD只會調用該句柄一次(不要覺得這樣會有問題,看完下面的內容你就明白了)。
用戶事件有兩種: DISPATCH_SOURCE_TYPE_DATA_ADD
和 DISPATCH_SOURCE_TYPE_DATA_OR
.用戶事件源有個 unsigned long data
屬性,我們將一個 unsigned long
傳入 dispatch_source_merge_data
。當使用 _ADD
版本時,事件在聯結時會把這些數字相加。當使用 _OR
版本時,事件在聯結時會把這些數字邏輯與運算。當事件句柄執行時,我們可以使用dispatch_source_get_data函數訪問當前值,然後這個值會被重置爲0。
讓我假設一種情況。假設一些異步執行的代碼會更新一個進度條。因爲主線程只不過是GCD的另一個dispatch queue而已,所以我們可以將GUI更新工作push到主線程中。然而,這些事件可能會有一大堆,我們不想對GUI進行頻繁而累贅的更新,理想的情況是當主線程繁忙時將所有的改變聯結起來。
用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我們可以將工作拼接起來,然後主線程可以知道從上一次處理完事件到現在一共發生了多少改變,然後將這一整段改變一次更新至進度條。
啥也不說了,上代碼:
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);
});
(對於這段代碼,我很想說點什麼,我第一次用dispatch source時,我糾結了很久很久,真讓人蛋疼:Dispatch source啓動時默認狀態是掛起的,我們創建完畢之後得主動恢復,否則事件不會被傳遞,也不會被執行)
假設你已經將進度條的min/max值設置好了,那麼這段代碼就完美了。數據會被併發處理。當每一段數據完成後,會通知dispatch source並將dispatch source data加1,這樣我們就認爲一個單元的工作完成了。事件句柄根據已完成的工作單元來更新進度條。若主線程比較空閒並且這些工作單元進行的比較慢,那麼事件句柄會在每個工作單元完成的時候被調用,實時更新。如果主線程忙於其他工作,或者工作單元完成速度很快,那麼完成事件會被聯結起來,導致進度條只在主線程變得可用時才被更新,並且一次將積累的改變更新至GUI。
現在你可能會想,聽起來倒是不錯,但是要是我不想讓事件被聯結呢?有時候你可能想讓每一次信號都會引起響應,什麼後臺的智能玩意兒統統不要。啊。。其實很簡單的,別把自己繞進去了。如果你想讓每一個信號都得到響應,那使用dispatch_async函數不就行了。實際上,使用的dispatch source而不使用dispatch_async的唯一原因就是利用聯結的優勢。
內建事件
上面就是怎樣使用用戶事件,那麼內建事件呢?看看下面這個例子,用GCD讀取標準輸入:
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);
簡單的要死!因爲我們使用的是全局隊列,句柄自動在後臺執行,與程序的其他部分並行,這意味着對這種情況的提速:事件進入程序時,程序正在處理其他事務。
這是標準的UNIX方式來處理事務的好處,不用去寫loop。如果使用經典的 read
調用,我們還得萬分留神,因爲返回的數據可能比請求的少,還得忍受無厘頭的“errors”,比如 EINTR
(系統調用中斷)。使用GCD,我們啥都不用管,就從這些蛋疼的情況裏解脫了。如果我們在文件描述符中留下了未讀取的數據,GCD會再次調用我們的句柄。
對於標準輸入,這沒什麼問題,但是對於其他文件描述符,我們必須考慮在完成讀寫之後怎樣清除描述符。對於dispatch source還處於活躍狀態時,我們決不能關閉描述符。如果另一個文件描述符被創建了(可能是另一個線程創建的)並且新的描述符剛好被分配了相同的數字,那麼你的dispatch source可能會在不應該的時候突然進入讀寫狀態。de這個bug可不是什麼好玩的事兒。
適當的清除方式是使用 dispatch_source_set_cancel_handler
,並傳入一個block來關閉文件描述符。然後我們使用 dispatch_source_cancel
來取消dispatch source,使得句柄被調用,然後文件描述符被關閉。
使用其他dispatch source類型也差不多。總的來說,你提供一個source(mach port、文件描述符、進程ID等等)的區分符來作爲diapatch source的句柄。mask參數通常不會被使用,但是對於 DISPATCH_SOURCE_TYPE_PROC
來說mask指的是我們想要接受哪一種進程事件。然後我們提供一個句柄,然後恢復這個source(前面我加粗字體所說的,得先恢復),搞定。dispatch source也提供一個特定於source的data,我們使用 dispatch_source_get_data
函數來訪問它。例如,文件描述符會給出大致可用的字節數。進程source會給出上次調用之後發生的事件的mask。具體每種source給出的data的含義,看man
page吧。
計時器
計時器事件稍有不同。它們不使用handle/mask參數,計時器事件使用另外一個函數 dispatch_source_set_timer
來配置計時器。這個函數使用三個參數來控制計時器觸發:
start
參數控制計時器第一次觸發的時刻。參數類型是 dispatch_time_t
,這是一個opaque類型,我們不能直接操作它。我們得需要dispatch_time
和 dispatch_walltime
函數來創建它們。另外,常量 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
通常很有用。
interval
參數沒什麼好解釋的。
leeway
參數比較有意思。這個參數告訴系統我們需要計時器觸發的精準程度。所有的計時器都不會保證100%精準,這個參數用來告訴系統你希望系統保證精準的努力程度。如果你希望一個計時器沒五秒觸發一次,並且越準越好,那麼你傳遞0爲參數。另外,如果是一個週期性任務,比如檢查email,那麼你會希望每十分鐘檢查一次,但是不用那麼精準。所以你可以傳入60,告訴系統60秒的誤差是可接受的。
這樣有什麼意義呢?簡單來說,就是降低資源消耗。如果系統可以讓cpu休息足夠長的時間,並在每次醒來的時候執行一個任務集合,而不是不斷的醒來睡去以執行任務,那麼系統會更高效。如果傳入一個比較大的leeway給你的計時器,意味着你允許系統拖延你的計時器來將計時器任務與其他任務聯合起來一起執行。
總結
現在你知道怎樣使用GCD的dispatch source功能來監視文件描述符、計時器、聯結的用戶事件以及其他類似的行爲。由於dispatch source完全與dispatch queue相集成,所以你可以使用任意的dispatch queue。你可以將一個dispatch source的句柄在主線程中執行、在全局隊列中併發執行、或者在用戶隊列中串行執行(執行時會將程序的其他模塊的運算考慮在內)。
下一篇我會討論如何對dispatch queue進行掛起、恢復、重定目標操作;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能。