Dispatch Sources
現代系統通常提供異步接口,允許應用向系統提交請求,然后在系統處理請求時應用可以繼續處理自己的事情。Grand Central Dispatch正是基于這個基本行為而設計,允許你提交請求,并通過block和dispatch queue報告結果。
dispatch source是基礎數據類型,協調特定底層系統事件的處理。Grand Central Dispatch支持以下dispatch source:
Timer dispatch source:定期產生通知
Signal dispatch source:UNIX信號到達時產生通知
Descriptor dispatch source:各種文件和socket操作的通知
數據可讀
數據可寫
文件在文件系統中被刪除、移動、重命名
文件元數據信息改變
Process dispatch source:進程相關的事件通知
當進程退出時
當進程發起fork或exec等調用
信號被遞送到進程
Mach port dispatch source:Mach相關事件的通知
Custom dispatch source:你自己定義并自己觸發
Dispatch source替代了異步回調函數,來處理系統相關的事件。當你配置一個dispatch source時,你指定要監測的事件、dispatch queue、以及處理事件的代碼(block或函數)。當事件發生時,dispatch source會提交你的block或函數到指定的queue去執行
和手工提交到queue的任務不同,dispatch source為應用提供連續的事件源。除非你顯式地取消,dispatch source會一直保留與dispatch queue的關聯。只要相應的事件發生,就會提交關聯的代碼到dispatch queue去執行。
為了防止事件積壓到dispatch queue,dispatch source實現了事件合并機制。如果新事件在上一個事件處理器出列并執行之前到達,dispatch source會將新舊事件的數據合并。根據事件類型的不同,合并操作可能會替換舊事件,或者更新舊事件的信息。
創建Dispatch Source
創建dispatch source需要同時創建事件源和dispatch source本身。事件源是處理事件所需要的native數據結構,例如基于描述符的dispatch source,你需要打開描述符;基于進程的事件,你需要獲得目標程序的進程ID。
然后可以如下創建相應的dispatch source:
使用 dispatch_source_create 函數創建dispatch source
配置dispatch source:
為dispatch source設置一個事件處理器
對于定時器源,使用 dispatch_source_set_timer 函數設置定時器信息
為dispatch source賦予一個取消處理器(可選)調用 dispatch_resume 函數開始處理事件由于dispatch source必須進行額外的配置才能被使用,dispatch_source_create 函數返回的dispatch source將處于掛起狀態。此時dispatch source會接收事件,但是不會進行處理。這時候你可以安裝事件處理器,并執行額外的配置。
編寫和安裝一個事件處理器
你需要定義一個事件處理器來處理事件,可以是函數或block對象,并使用 dispatch_source_set_event_handler 或 dispatch_source_set_event_handler_f 安裝事件處理器。事件到達時,dispatch source會提交你的事件處理器到指定的dispatch queue,由queue執行事件處理器。
事件處理器的代碼負責處理所有到達的事件。如果事件處理器已經在queue中并等待處理已經到達的事件,如果此時又來了一個新事件,dispatch source會合并這兩個事件。事件處理器通常只能看到最新事件的信息,不過某些類型的dispatch source也能獲得已經發生以及合并的事件信息。
如果事件處理器已經開始執行,一個或多個新事件到達,dispatch source會保留這些事件,直到前面的事件處理器完成執行。然后以新事件再次提交處理器到queue。
函數事件處理器有一個context指針指向dispatch source對象,沒有返回值。Block事件處理器沒有參數,也沒有返回值。
// Block-based event handler
void (^dispatch_block_t)(void)
// Function-based event handler
void (*dispatch_function_t)(void *)
在事件處理器中,你可以從dispatch source中獲得事件的信息,函數處理器可以直接使用參數指針,Block則必須自己捕獲到dispatch source指針,一般block定義時會自動捕獲到外部定義的所有變量。
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// Get some data from the source variable, which is captured
// from the parent context.
size_t estimated = dispatch_source_get_data(source);
// Continue reading the descriptor...
});
dispatch_resume(source);
Block捕獲外部變量允許更大的靈活性和動態性。當然,在Block中這些變量默認是只讀的,雖然可以使用__block來修改捕獲的變量,但是你最好不要在事件處理器中這樣做。因為Dispatch source異步執行事件處理器,當事件處理器修改原始外部變量時,有可能這些變量已經不存在了。
下面是事件處理器能夠獲得的事件信息:
函數 描述
dispatch_source_get_handle 這個函數返回dispatch source管理的底層系統數據類型。
對于描述符dispatch source,函數返回一個int,表示關聯的描述符
對于信號dispatch source,函數返回一個int,表示最新事件的信號數值
對于進程dispatch source,函數返回一個pid_t數據結構,表示被監控的進程
對于Mach port dispatch source,函數返回一個 mach_port_t 數據結構
對于其它dispatch source,函數返回的值未定義
dispatch_source_get_data 這個函數返回事件關聯的所有未決數據。
對于從文件中讀取數據的描述符dispatch source,這個函數返回可以讀取的字節數
對于向文件中寫入數據的描述符dispatch source,如果可以寫入,則返回正數值
對于監控文件系統活動的描述符dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_vnode_flags_t 枚舉類型
對于進程dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_proc_flags_t 枚舉類型
對于Mach port dispatch source,函數返回一個常量,表示發生的事件類型,參考 dispatch_source_machport_flags_t 枚舉類型
對于自定義dispatch source,函數返回從現有數據創建的新數據,以及傳遞給 dispatch_source_merge_data 函數的新數據。
dispatch_source_get_mask 這個函數返回用來創建dispatch source的事件標志
對于進程dispatch source,函數返回dispatch source接收到的事件掩碼,參考 dispatch_source_proc_flags_t 枚舉類型
對于發送權利的Mach port dispatch source,函數返回期望事件的掩碼,參考 dispatch_source_mach_send_flags_t 枚舉類型
對于自定義 “或” 的dispatch source,函數返回用來合并數據值的掩碼。
安裝一個取消處理器
取消處理器在dispatch soruce釋放之前執行清理工作。多數類型的dispatch source不需要取消處理器,除非你對dispatch source有自定義行為需要在釋放時執行。但是使用描述符或Mach port的dispatch source必須設置取消處理器,用來關閉描述符或釋放Mach port。否則可能導致微妙的bug,這些結構體會被系統其它部分或你的應用在不經意間重用。
你可以在任何時候安裝取消處理器,但通常我們在創建dispatch source時就會安裝取消處理器。使用 dispatch_source_set_cancel_handler 或 dispatch_source_set_cancel_handler_f 函數來設置取消處理器。
下面取消處理器關閉描述符:
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});
修改目標Queue
在創建dispatch source時可以指定一個queue,用來執行事件處理器和取消處理器。不過你也可以使用 dispatch_set_target_queue 函數在任何時候修改目標queue。修改queue可以改變執行dispatch source事件的優先級。
修改dispatch source的目標queue是異步操作,dispatch source會盡可能快地完成這個修改。如果事件處理器已經進入queue并等待處理,它會繼續在原來的Queue中執行。隨后到達的所有事件的處理器都會在后面修改的queue中執行。
關聯自定義數據到dispatch source
和Grand Central Dispatch的其它類型一樣,你可以使用 dispatch_set_context 函數關聯自定義數據到dispatch source。使用context指針存儲事件處理器需要的任何數據。如果你在context指針中存儲了數據,你就應該安裝一個取消處理器,在dispatch source不再需要時釋放這些context自定義數據。
如果你使用block實現事件處理器,你也可以捕獲本地變量,并在Block中使用。雖然這樣也可以代替context指針,但是你應該明智地使用Block捕獲變量。因為dispatch source長時間存在于應用中,Block捕獲指針變量時必須非常小心,因為指針指向的數據可能會被釋放,因此需要復制數據或retain。不管使用哪種方法,你都應該提供一個取消處理器,在最后釋放這些數據。
Dispatch Source的內存管理
Dispatch Source也是引用計數的數據類型,初始計數為1,可以使用 dispatch_retain 和 dispatch_release 函數來增加和減少引用計數。引用計數到達0時,系統自動釋放dispatch source數據結構。
dispatch source的所有權可以由dispatch source內部或外部進行管理。外部所有權時,另一個對象擁有dispatch source,并負責在不需要時釋放它。內部所有權時,dispatch source自己擁有自己,并負責在適當的時候釋放自己。雖然外部所有權很常用,當你希望創建自主dispatch source,并讓它自己管理自己的行為時,可以使用內部所有權。例如dispatch source應用單一全局事件時,可以讓它自己處理該事件,并立即退出。
Dispatch Source示例
創建一個定時器
定時器dispatch source定時產生事件,可以用來發起定時執行的任務,如游戲或其它圖形應用,可以使用定時器來更新屏幕或動畫。你也可以設置定時器,并在固定間隔事件中檢查服務器的新信息。
所有定時器dispatch source都是間隔定時器,一旦創建,會按你指定的間隔定期遞送事件。你需要為定時器dispatch source指定一個期望的定時器事件精度,也就是leeway值,讓系統能夠靈活地管理電源并喚醒內核。例如系統可以使用leeway值來提前或延遲觸發定時器,使其更好地與其它系統事件結合。創建自己的定時器時,你應該盡量指定一個leeway值。
就算你指定leeway值為0,也不要期望定時器能夠按照精確的納秒來觸發事件。系統會盡可能地滿足你的需求,但是無法保證完全精確的觸發時間。
當計算機睡眠時,定時器dispatch source會被掛起,稍后系統喚醒時,定時器dispatch source也會自動喚醒。根據你提供的配置,暫停定時器可能會影響定時器下一次的觸發。如果定時器dispatch source使用 dispatch_time 函數或 DISPATCH_TIME_NOW 常量設置,定時器dispatch source會使用系統默認時鐘來確定何時觸發,但是默認時鐘在計算機睡眠時不會繼續。
如果你使用 dispatch_walltime 函數來設置定時器dispatch source,則定時器會根據掛鐘時間來跟蹤,這種定時器比較適合觸發間隔相對比較大的場合,可以防止定時器觸發間隔出現太大的誤差。
下面是定時器dispatch source的一個例子,每30秒觸發一次,leeway值為1,因為間隔相對較大,使用 dispatch_walltime 來創建定時器。定時器會立即觸發第一次,隨后每30秒觸發一次。 MyPeriodicTask 和 MyStoreTimer 是自定義函數,用于實現定時器的行為,并存儲定時器到應用的數據結構。
dispatch_source_t CreateDispatchTimer(uint64_t interval,
uint64_t leeway,
dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
void MyCreateTimer()
{
dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
1ull * NSEC_PER_SEC,
dispatch_get_main_queue(),
^{ MyPeriodicTask(); });
// Store it somewhere for later use.
if (aTimer)
{
MyStoreTimer(aTimer);
}
}
雖然定時器dispatch source是接收時間事件的主要方法,你還可以使用其它選擇。如果想在指定時間間隔后執行一個block,可以使用 dispatch_after 或 dispatch_after_f 函數。這兩個函數非常類似于dispatch_async,但是只允許你指定一個時間值,時間一到就自動提交block到queue中執行,時間值可以指定為相對或絕對時間。
從描述符中讀取數據
要從文件或socket中讀取數據,需要打開文件或socket,并創建一個 DISPATCH_SOURCE_TYPE_READ 類型的dispatch source。你指定的事件處理器必須能夠讀取和處理描述符中的內容。對于文件,需要讀取文件數據,并為應用創建適當的數據結構;對于網絡socket,需要處理最新接收到的網絡數據。
讀取數據時,你總是應該配置描述符使用非阻塞操作,雖然你可以使用 dispatch_source_get_data 函數查看當前有多少數據可讀,但在你調用它和實際讀取數據之間,可用的數據數量可能會發生變化。如果底層文件被截斷,或發生網絡錯誤,從描述符中讀取會阻塞當前線程,停止在事件處理器中間并阻止dispatch queue去執行其它任務。對于串行queue,這樣還可能會死鎖,即使是并發queue,也會減少queue能夠執行的任務數量。
下面例子配置dispatch source從文件中讀取數據,事件處理器讀取指定文件的全部內容到緩沖區,并調用一個自定義函數來處理這些數據。調用方可以使用返回的dispatch source在讀取操作完成之后,來取消這個事件。為了確保dispatch queue不會阻塞,這里使用了fcntl函數,配置文件描述符執行非阻塞操作。dispatch source安裝了取消處理器,確保最后關閉了文件描述符。
dispatch_source_t ProcessContentsOfFile(const char* filename)
{
// Prepare the file for reading.
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK);? // Avoid blocking the read operation
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}
// Install the event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// Read the data into a text buffer.
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual);? // Process the data.
// Release the buffer when done.
free(buffer);
// If there is no more data, cancel the source.
if (done)
dispatch_source_cancel(readSource);
}
});
// Install the cancellation handler
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});
// Start reading the file.
dispatch_resume(readSource);
return readSource;
}
在這個例子中,自定義的 MyProcessFileData 函數確定讀取到足夠的數據,返回YES告訴dispatch source讀取已經完成,可以取消任務。通常讀取描述符的dispatch source在還有數據可讀時,會重復調度事件處理器。如果socket連接關閉或到達文件末尾,dispatch source自動停止調度事件處理器。如果你自己確定不再需要dispatch source,也可以手動取消它。
向描述符寫入數據
向文件或socket寫入數據非常類似于讀取數據,配置描述符為寫入操作后,創建一個 DISPATCH_SOURCE_TYPE_WRITE 類型的dispatch source,創建好之后,系統會調用事件處理器,讓它開始向文件或socket寫入數據。當你完成寫入后,使用 dispatch_source_cancel 函數取消dispatch source。
寫入數據也應該配置文件描述符使用非阻塞操作,雖然 dispatch_source_get_data 函數可以查看當前有多少可用寫入空間,但這個值只是建議性的,而且在你執行寫入操作時可能會發生變化。如果發生錯誤,寫入數據到阻塞描述符,也會使事件處理器停止在執行中途,并阻止dispatch queue執行其它任務。串行queue會產生死鎖,并發queue則會減少能夠執行的任務數量。
下面是使用dispatch source寫入數據到文件的例子,創建文件后,函數傳遞文件描述符到事件處理器。MyGetData函數負責提供要寫入的數據,在數據寫入到文件之后,事件處理器取消dispatch source,阻止再次調用。此時dispatch source的擁有者需負責釋放dispatch source。
dispatch_source_t WriteDataToFile(const char* filename)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
(S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL); // Block during the write.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,
fd, 0, queue);
if (!writeSource)
{
close(fd);
return NULL;
}
dispatch_source_set_event_handler(writeSource, ^{
size_t bufferSize = MyGetDataSize();
void* buffer = malloc(bufferSize);
size_t actual = MyGetData(buffer, bufferSize);
write(fd, buffer, actual);
free(buffer);
// Cancel and release the dispatch source when done.
dispatch_source_cancel(writeSource);
});
dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
dispatch_resume(writeSource);
return (writeSource);
}
監控文件系統對象
如果需要監控文件系統對象的變化,可以設置一個 DISPATCH_SOURCE_TYPE_VNODE 類型的dispatch source,你可以從這個dispatch source中接收文件刪除、寫入、重命名等通知。你還可以得到文件的特定元數據信息變化通知。
在dispatch source正在處理事件時,dispatch source中指定的文件描述符必須保持打開狀態。
下面例子監控一個文件的文件名變化,并在文件名變化時執行一些操作(自定義的 MyUpdateFileName 函數)。由于文件描述符專門為dispatch source打開,dispatch source安裝了取消處理器來關閉文件描述符。這個例子中的文件描述符關聯到底層的文件系統對象,因此同一個dispatch source可以用來檢測多次文件名變化。
dispatch_source_t MonitorNameChangesToFile(const char* filename)
{
int fd = open(filename, O_EVTONLY);
if (fd == -1)
return NULL;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
fd, DISPATCH_VNODE_RENAME, queue);
if (source)
{
// Copy the filename for later use.
int length = strlen(filename);
char* newString = (char*)malloc(length + 1);
newString = strcpy(newString, filename);
dispatch_set_context(source, newString);
// Install the event handler to process the name change
dispatch_source_set_event_handler(source, ^{
const char*? oldFilename = (char*)dispatch_get_context(source);
MyUpdateFileName(oldFilename, fd);
});
// Install a cancellation handler to free the descriptor
// and the stored string.
dispatch_source_set_cancel_handler(source, ^{
char* fileStr = (char*)dispatch_get_context(source);
free(fileStr);
close(fd);
});
// Start processing events.
dispatch_resume(source);
}
else
close(fd);
return source;
}
監測信號
應用可以接收許多不同類型的信號,如不可恢復的錯誤(非法指令)、或重要信息的通知(如子進程退出)。傳統編程中,應用使用 sigaction 函數安裝信號處理器函數,信號到達時同步處理信號。如果你只是想信號到達時得到通知,并不想實際地處理該信號,可以使用信號dispatch source來異步處理信號。
信號dispatch source不能替代 sigaction 函數提供的同步信號處理機制。同步信號處理器可以捕獲一個信號,并阻止它中止應用。而信號dispatch source只允許你監測信號的到達。此外,你不能使用信號dispatch source獲取所有類型的信號,如SIGILL, SIGBUS, SIGSEGV信號。
由于信號dispatch source在dispatch queue中異步執行,它沒有同步信號處理器的一些限制。例如信號dispatch source的事件處理器可以調用任何函數。靈活性增大的代價是,信號到達和dispatch source事件處理器被調用的延遲可能會增大。
下面例子配置信號dispatch source來處理SIGHUP信號,事件處理器調用 MyProcessSIGHUP 函數,用來處理信號。
void InstallSignalHandler()
{
// Make sure the signal does not terminate the application.
signal(SIGHUP, SIG_IGN);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue);
if (source)
{
dispatch_source_set_event_handler(source, ^{
MyProcessSIGHUP();
});
// Start processing signals
dispatch_resume(source);
}
}
監控進程
進程dispatch source可以監控特定進程的行為,并適當地響應。父進程可以使用dispatch source來監控自己創建的所有子進程,例如監控子進程的死亡;類似地,子進程也可以使用dispatch source來監控父進程,例如在父進程退出時自己也退出。
下面例子安裝了一個進程dispatch source,監控父進程的終止。當父進程退出時,dispatch source設置一些內部狀態信息,告知子進程自己應該退出。MySetAppExitFlag 函數應該設置一個適當的標志,允許子進程終止。由于dispatch source自主運行,因此自己擁有自己,在程序關閉時會取消并釋放自己。
void MonitorParentProcess()
{
pid_t parentPID = getppid();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
parentPID, DISPATCH_PROC_EXIT, queue);
if (source)
{
dispatch_source_set_event_handler(source, ^{
MySetAppExitFlag();
dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);
}
}
取消一個Dispatch Source
除非你顯式地調用 dispatch_source_cancel 函數,dispatch source將一直保持活動,取消一個dispatch source會停止遞送新事件,并且不能撤銷。因此你通常在取消dispatch source后立即釋放它:
void RemoveDispatchSource(dispatch_source_t mySource)
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}
取消一個dispatch source是異步操作,調用 dispatch_source_cancel 之后,不會再有新的事件被處理,但是正在被dispatch source處理的事件會繼續被處理完成。在處理完最后的事件之后,dispatch source會執行自己的取消處理器。
取消處理器是你最后的執行機會,在那里執行內存或資源的釋放工作。例如描述符或mach port類型的dispatch source,必須提供取消處理器,用來關閉描述符或mach port
掛起和繼續Dispatch Source
你可以使用 dispatch_suspend 和 dispatch_resume 臨時地掛起和繼續dispatch source的事件遞送。這兩個函數分別增加和減少dispatch 對象的掛起計數。因此,你必須每次 dispatch_suspend 調用之后,都需要相應的 dispatch_resume 才能繼續事件遞送。
掛起一個dispatch source期間,發生的任何事件都會被累積,直到dispatch source繼續。但是不會遞送所有事件,而是先合并到單一事件,然后再一次遞送。例如你監控一個文件的文件名變化,就只會遞送最后一次的變化事件。