iOS Concurrency Programming Guide
iOS 和 Mac OS 傳統的並發編程模型是線程,不過線程模型伸縮性不強,而且編寫正確的線程代碼也不容易。Mac OS 和 iOS 采取 asynchronous design approach 來解決並發的問題。
引入的異步技術有兩個:
Grand Central Dispatch:系統管理線程,你不需要編寫線程代碼。只需定義想要執行的任務,然后添加到適當的dispatch queue。Grand Central Dispatch會負責創建線程和調度你的任務。系統直接提供線程管理,比應用實現更加高效。
Operation Queue:Objective-C對象,類似於dispatch queue。你定義想要執行的任務,並添加任務到operation queue,后者負責調度和執行這些任務。和Grand Central Dispatch一樣,Operation Queue也管理了線程,更加高效。
Dispatch Queue
基於C的執行自定義任務機制。dispatch queue按先進先出的順序,串行或並發地執行任務。serial dispaptch queue一次只能執行一個任務,直接當前任務完成才開始出列並啟動下一個任務。而concurrent dispatch queue則盡可能多地啟動任務並發執行。
優點:
直觀而簡單的編程接口
提供自動和整體的線程池管理
提供匯編級調優的速度
更加高效地使用內存
不會trap內核under load
異步分派任務到dispatch queue不會導致queue死鎖
伸縮性強
serial dispatch queue比鎖和其它同步原語更加高效
Dispatch Sources
Dispatch Sources 是基於C的系統事件異步處理機制。一個Dispatch Source封裝了一個特定類型的系統事件,當事件發生時提交一個特定的block對象或函數到dispatch queue。你可以使用Dispatch Sources監控以下類型的系統事件:
定時器
信號處理器
描述符相關的事件
進程相關的事件
Mach port事件
你觸發的自定義事件
Operation Queues
Operation Queues是Cocoa版本的並發dispatch queue,由 NSOperationQueue 類實現。dispatch queue總是按先進先出的順序執行任務,而 Operation Queues 在確定任務執行順序時,還會考慮其它因素。最主要的一個因素是指定任務是否依賴於另一個任務的完成。你在定義任務時配置依賴性,從而創建復雜的任務執行順序圖
提交到Operation Queues的任務必須是 NSOperation 對象,operation object封裝了你要執行的工作,以及所需的所有數據。由於 NSOperation 是一個抽象基類,通常你需要定義自定義子類來執行任務。不過Foundation framework自帶了一些具體子類,你可以創建並執行相關的任務。
Operation objects會產生key-value observing(KVO)通知,對於監控任務的進程非常有用。雖然operation queue總是並發地執行任務,你可以使用依賴,在需要時確保順序執行
異步設計技術
通過確保主線程自由響應用戶事件,並發可以很好地提高應用的響應性。通過將工作分配到多核,還能提高應用處理的性能。但是並發也帶來一定的額外開銷,並且使代碼更加復雜,更難編寫和調試代碼。
因此在應用設計階段,就應該考慮並發,設計應用需要執行的任務,及任務所需的數據結構。
Operation Queues
基於Objective-C,因此基於Cocoa的應用通常會使用Operation Queues
Operation Objects
operation object 是 NSOperation 類的實例,封裝了應用需要執行的任務,和執行任務所需的數據。NSOperation 本身是抽象基類,我們必須實現子類。Foundation framework提供了兩個具體子類,你可以直接使用:
類 | 描述 |
NSInvocationOperation | 可以直接使用的類,基於應用的一個對象和selector來創建operation object。如果你已經有現有的方法來執行需要的任務,就可以使用這個類。 |
NSBlockOperation | 可以直接使用的類,用來並發地執行一個或多個block對象。operation object使用“組”的語義來執行多個block對象,所有相關的block都執行完成之后,operation object才算完成。 |
NSOperation | 基類,用來自定義子類operation object。繼承NSOperation可以完全控制operation object的實現,包括修改操作執行和狀態報告的方式。 |
所有operation objects都支持以下關鍵特性:
支持建立基於圖的operation objects依賴。可以阻止某個operation運行,直到它依賴的所有operation都已經完成。
支持可選的completion block,在operation的主任務完成后調用。
支持應用使用KVO通知來監控operation的執行狀態。
支持operation優先級,從而影響相對的執行順序
支持取消,允許你中止正在執行的任務
並發 VS 非並發Operations
通常我們通過將operation添加到operation queue中來執行該操作。但是我們也可以手動調用start方法來執行一個operation對象,這樣做不保證operation會並發執行。NSOperation類對象的 isConcurrent 方法告訴你這個operation相對於調用start方法的線程,是同步還是異步執行的。isConcurrent 方法默認返回NO,表示operation與調用線程同步執行。
如果你需要實現並發operation,也就是相對調用線程異步執行的操作。你必須添加額外的代碼,來異步地啟動操作。例如生成一個線程、調用異步系統函數,以確保start方法啟動任務,並立即返回。
多數開發者從來都不需要實現並發operation對象,我們只需要將operations添加到operation queue。當你提交非並發operation到operation queue時,queue會創建線程來運行你的操作,因此也能達到異步執行的目的。只有你不希望使用operation queue來執行operation時,才需要定義並發operations。
創建一個 NSInvocationOperation 對象
如果已經現有一個方法,需要並發地執行,就可以直接創建 NSInvocationOperation 對象,而不需要自己繼承 NSOperation。
- @implementation MyCustomClass
- - (NSOperation*)taskWithData:(id)data {
- NSInvocationOperation* theOp = [[[NSInvocationOperation alloc] initWithTarget:self
- selector:@selector(myTaskMethod:) object:data] autorelease];
- return theOp;
- }
- // This is the method that does the actual work of the task.
- - (void)myTaskMethod:(id)data {
- // Perform the task.
- }
- @end
創建一個 NSBlockOperation 對象
NSBlockOperation 對象用於封裝一個或多個block對象,一般創建時會添加至少一個block,然后再根據需要添加更多的block。當 NSBlockOperation 對象執行時,會把所有block提交到默認優先級的並發dispatch queue。然后 NSBlockOperation 對象等待所有block完成執行,最后標記自己已完成。因此可以使用block operation來跟蹤一組執行中的block,有點類似於thread join等待多個線程的結果。區別在於block operation本身也運行在一個單獨的線程,應用的其它線程在等待block operation完成時可以繼續工作。
- NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
- NSLog(@"Beginning operation.\n");
- // Do some work.
- }];
使用 addExecutionBlock: 可以添加更多block到這個block operation對象。如果需要順序地執行block,你必須直接提交到所需的dispatch queue。
自定義Operation對象
如果block operation和invocation operation對象不符合應用的需求,你可以直接繼承 NSOperation,並添加任何你想要的行為。NSOperation 類提供通用的子類繼承點,而且實現了許多重要的基礎設施來處理依賴和KVO通知。繼承所需的工作量主要取決於你要實現非並發還是並發的operation。
定義非並發operation要簡單許多,只需要執行主任務,並正確地響應取消事件;NSOperation 處理了其它所有事情。對於並發operation,你必須替換某些現有的基礎設施代碼。
執行主任務
每個operation對象至少需要實現以下方法:
自定義initialization方法:初始化,將operation 對象設置為已知狀態
自定義main方法:執行你的任務
你也可以選擇性地實現以下方法:
main方法中需要調用的其它自定義方法
Accessor方法:設置和訪問operation對象的數據
dealloc方法:清理operation對象分配的所有內存
NSCoding 協議的方法:允許operation對象archive和unarchive
- @interface MyNonConcurrentOperation : NSOperation {
- id myData;
- }
- -(id)initWithData:(id)data;
- @end
- @implementation MyNonConcurrentOperation
- - (id)initWithData:(id)data {
- if (self = [super init])
- myData = [data retain];
- return self;
- }
- - (void)dealloc {
- [myData release];
- [super dealloc];
- }
- -(void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do some work on myData and report the results.
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
- @end
響應取消事件
operation開始執行之后,會一直執行任務直到完成,或者顯式地取消操作。取消可能在任何時候發生,甚至在operation執行之前。盡管 NSOperation 提供了一個方法,讓應用取消一個操作,但是識別出取消事件則是你的事情。如果operation直接終止,可能無法回收所有已分配的內存或資源。因此operation對象需要檢測取消事件,並優雅地退出執行。
operation 對象定期地調用 isCancelled 方法,如果返回YES(表示已取消),則立即退出執行。不管是自定義 NSOperation 子類,還是使用系統提供的兩個具體子類,都需要支持取消。isCancelled方法本身非常輕量,可以頻繁地調用而不產生大的性能損失。以下地方可能需要調用isCancelled:
在執行任何實際的工作之前
在循環的每次迭代過程中,如果每個迭代相對較長可能需要調用多次
代碼中相對比較容易中止操作的任何地方
- - (void)main {
- @try {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- BOOL isDone = NO;
- while (![self isCancelled] && !isDone) {
- // Do some work and set isDone to YES when finished
- }
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
注意你的代碼還需要完成所有相關的資源清理工作
為並發執行配置operations
Operation對象默認按同步方式執行,也就是在調用start方法的那個線程中直接執行。由於operation queue為非並發operation提供了線程支持,對應用來說,多數operations仍然是異步執行的。但是如果你希望手工執行operations,而且仍然希望能夠異步執行操作,你就必須采取適當的措施,通過定義operation對象為並發操作來實現。
方法 | 描述 |
start | (必須)所有並發操作都必須覆蓋這個方法,以自定義的實現替換默認行為。手動執行一個操作時,你會調用start方法。因此你對這個方法的實現是操作的起點,設置一個線程或其它執行環境,來執行你的任務。你的實現在任何時候都絕對不能調用super。 |
main | (可選)這個方法通常用來實現operation對象相關聯的任務。盡管你可以在start方法中執行任務,使用main來實現任務可以讓你的代碼更加清晰地分離設置和任務代碼 |
isExecuting isFinished |
(必須)並發操作負責設置自己的執行環境,並向外部client報告執行環境的狀態。因此並發操作必須維護某些狀態信息,以知道是否正在執行任務,是否已經完成任務。使用這兩個方法報告自己的狀態。 這兩個方法的實現必須能夠在其它多個線程中同時調用。另外這些方法報告的狀態變化時,還需要為相應的key path產生適當的KVO通知。 |
isConcurrent | (必須)標識一個操作是否並發operation,覆蓋這個方法並返回YES |
- @interface MyOperation : NSOperation {
- BOOL executing;
- BOOL finished;
- }
- - (void)completeOperation;
- @end
- @implementation MyOperation
- - (id)init {
- self = [super init];
- if (self) {
- executing = NO;
- finished = NO;
- }
- return self;
- }
- - (BOOL)isConcurrent {
- return YES;
- }
- - (BOOL)isExecuting {
- return executing;
- }
- - (BOOL)isFinished {
- return finished;
- }
- - (void)start {
- // Always check for cancellation before launching the task.
- if ([self isCancelled])
- {
- // Must move the operation to the finished state if it is canceled.
- [self willChangeValueForKey:@"isFinished"];
- finished = YES;
- [self didChangeValueForKey:@"isFinished"];
- return;
- }
- // If the operation is not canceled, begin executing the task.
- [self willChangeValueForKey:@"isExecuting"];
- [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
- executing = YES;
- [self didChangeValueForKey:@"isExecuting"];
- }
- - (void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do the main work of the operation here.
- [self completeOperation];
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
- - (void)completeOperation {
- [self willChangeValueForKey:@"isFinished"];
- [self willChangeValueForKey:@"isExecuting"];
- executing = NO;
- finished = YES;
- [self didChangeValueForKey:@"isExecuting"];
- [self didChangeValueForKey:@"isFinished"];
- }
- @end
即使操作被取消,你也應該通知KVO observers,你的操作已經完成。當某個operation對象依賴於另一個operation對象的完成時,它會監測后者的isFinished key path。只有所有依賴的對象都報告已經完成,第一個operation對象才會開始運行。如果你的operation對象沒有產生完成通知,就會阻止其它依賴於你的operation對象運行。
維護KVO依從
NSOperation類的key-value observing(KVO)依從於以下key paths:
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock
如果你覆蓋start方法,或者對NSOperation對象的其它自定義運行(覆蓋main除外),你必須確保自定義對象對這些key paths保留KVO依從。覆蓋start方法時,需要關注isExecuting和isFinished兩個key paths。
如果你希望實現依賴於其它東西(非operation對象),你可以覆蓋isReady方法,並強制返回NO,直到你等待的依賴得到滿足。如果你需要保留默認的依賴管理系統,確保你調用了[super isReady]。當你的operation對象的准備就緒狀態發生改變時,生成一個isReady的key path的KVO通知。
除非你覆蓋了 addDependency: 或 removeDependency: 方法,否則你不需要關注dependencies key path
雖然你也可以生成 NSOperation 的其它KVO通知,但通常你不需要這樣做。如果需要取消一個操作,你可以直接調用現有的cancel方法。類似地,你也很少需要修改queue優先級信息。最后,除非你的operation對象可以動態地改變並發狀態,你也不需要提供isConcurrent key path的KVO通知。
自定義一個Operation對象的執行行為
對Operation對象的配置發生在創建對象之后,將其添加到queue之前。
配置operation之間的依賴關系
依賴關系可以順序地執行相關的operation對象,依賴於其它操作,則必須等到該操作完成之后自己才能開始。你可以創建一對一的依賴關系,也可以創建多個對象之間的依賴圖。
使用 NSOperation 的 addDependency: 方法在兩個operation對象之間建立依賴關系。表示當前operation對象將依賴於參數指定的目標operation對象。依賴關系不局限於相同queue中的operations對象,Operation對象會管理自己的依賴,因此完全可以在不同的queue之間的Operation對象創建依賴關系。
唯一的限制是不能創建環形依賴,這是程序員的錯誤,所有受影響的operations都無法運行!
當一個operation對象依賴的所有其它對象都已經執行完成,該operation就變成准備執行狀態(如果你自定義了isReady方法,則由你的方法確定是否准備好運行)。如果operation已經在一個queue中,queue就可以在任何時候執行這個operation。如果你需要手動執行該operation,就自己調用operation的start方法。
配置依賴必須在運行operation和添加operation到queue之前進行,之后添加的依賴關系可能不起作用。
依賴要求每個operation對象在狀態發生變化時必須發出適當的KVO通知。如果你自定義了operation對象的行為,就必須在自定義代碼中生成適當的KVO通知,以確保依賴能夠正確地執行。
修改Operation的執行優先級
對於添加到queue的Operations,執行順序首先由已入隊列的operations是否准備好,然后再根據所有operations的相對優先級確定。是否准備好由對象的依賴關系確定,優先級等級則是operation對象本身的一個屬性。默認所有operation都擁有“普通”優先級,不過你可以通過 setQueuePriority: 方法來提升或降低operation對象的優先級。
優先級只能應用於相同queue中的operations。如果應用有多個operation queue,每個queue的優先級等級是互相獨立的。因此不同queue中的低優先級操作仍然可能比高優先級操作更早執行。
優先級不能替代依賴關系,優先級只是queue對已經准備好的operations確定執行順序。先滿足依賴關系,然后再根據優先級從所有准備好的操作中選擇優先級最高的那個執行。
修改底層線程的優先級
Mac OS X 10.6之后,我們可以配置operation底層線程的執行優先級,線程直接由內核管理,通常優先級高的線程會給予更多的執行機會。對於operation對象,你指定線程優先級為0.0到1.0之間的某個數值,0.0表示最低優先級,1.0表示最高優先級。默認線程優先級為0.5
要設置operation的線程優先級,你必須在將operation添加到queue之前,調用 setThreadPriority: 方法進行設置。當queue執行該operation時,默認的start方法會使用你指定的值來修改當前線程的優先級。不過新的線程優先級只在operation的main方法范圍內有效。其它所有代碼仍然(包括completion block)運行在默認線程優先級。
如果你創建了並發operation,並覆蓋了start方法,你必須自己配置線程優先級。
設置一個completion block
在Mac OS X 10.6之后,operation可以在主任務完成之后執行一個completion block。你可以使用這個completion block來執行任何不屬於主任務的工作。例如你可以使用這個block來通知相關的client,操作已經執行完成。而並發operation對象則可以使用這個block來產生最終的KVO通知。
調用 NSOperation 的 setCompletionBlock: 方法來設置一個completion block,你傳遞的block應該沒有參數和返回值。
實現Operation對象的技巧
Operation對象的內存管理
operation對象需要良好的內存管理策略
創建你自己的Autorelease Pool
operation是Objective-C對象,你在實現任務的代碼中應該創建一個autorelease pool,這樣可以保護那些autorelease對象得到盡快地釋放。雖然你的自定義代碼執行時可能已經有了一個pool,但你不能依賴於這個行為,總是應該自己創建一個。
擁有自己的autorelease pool還能更加靈活地管理operation的內存。如果operation創建大量的臨時對象,則可以考慮創建額外的pool,來清理不再使用的臨時對象。在iOS*****別需要注意,應遲早地清理不再使用的臨時對象,避免內存警告。
- - (void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do the main work of the operation here.
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
避免Per-Thread存儲
雖然多數operation都在線程中執行,但對於非並發operation,通常由operation queue提供線程,這時候queue擁有該線程,而你的應用不應該去動這個線程。特別是不要關聯任何數據到不是你創建和擁有的線程。這些線程由queue管理,根據系統和應用的需求創建或銷毀。因此使用Per-Thread storage在operations之間傳遞數據是不可靠的,而且很有可能會失敗。
對於operation對象,你完全沒有理由使用Per-Thread Storage,應該在創建對象的時候就給它需要的所有數據。所有輸入和輸出數據都應該存儲在operation對象中,最后再整合到你的應用,或者最終釋放掉。
根據需要保留Operation對象的引用
由於operation對象異步執行,你不能創建完以后就完全不管。它們也是對象,需要你來分配和釋放它們管理的任何資源,特別是如果你需要在operation對象完成后獲取其中的數據。
由於queue總是盡最大可能快速地調度和執行operation,在你添加operation到queue時,可能立即就開始運行,當你稍后向queue請求operation對象的狀態時,有可能queue已經執行完了相應的operation並從queue中刪除了這個對象。因此你總是應該自己擁有operation對象的引用。
處理錯誤和異常
operation本質上是應用中獨立的實體,因此需要自己負責處理所有的錯誤和異常。NSOperation默認的start方法並沒有捕獲異常。所以你自己的代碼總是應該捕獲並抑制異常。你還應該檢查錯誤代碼並適當地通知應用。如果你覆蓋了start方法,你也必須捕獲所有異常,阻止它離開底層線程的范圍。
你需要准備好處理以下錯誤或異常:
檢查並處理UNIX errno風格的錯誤代碼
檢查方法或函數顯式返回的錯誤代碼
捕獲你的代碼或系統frameworks拋出的異常
捕獲NSOperation類自己拋出的異常,在以下情況NSOperation會拋出異常:
operation沒有准備好,但是調用了start方法
operation正在執行或已經完成(可能被取消),再次調用了start方法。
當你添加completion block到正在執行或已經完成的operation
當你試圖獲取已經取消 NSInvocationOperation 對象的結果
為Operation對象確定一個適當的范圍
和任何對象一樣,NSOperation對象也會消耗內存,執行時也會帶來開銷。因此如果operation對象只做很少的工作,但是卻創建成千上萬個小的operation對象,你就會發現更多的時間花在了調度operations而不是執行它們。
要高效地使用Operations,關鍵是在Operation執行的工作量和保持計算機繁忙之間,找到最佳的平衡。確保每個Operation都有一定的工作量可以執行。例如100個operations執行100次相同任務,可以考慮換成10個operations,每個執行10次。
你同樣要避免向一個queue中添加過多的operations,或者持續快速地向queue中添加operation,超過queue所能處理的能力。這里可以考慮分批創建operations對象,在一批對象執行完之后,使用completion block告訴應用創建下一批operations對象。
執行Operations
應用需要執行Operations來處理相關的工作,你有幾種方法來執行Operations對象。
添加Operations到Operation Queue
執行Operations最簡單的方法是添加到operation queue,后者是 NSOperationQueue 對象。應用負責創建和維護自己使用的所有 NSOperationQueue 對象。
- NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
調用 addOperation: 方法添加一個operation到queue,Mac OS X 10.6之后可以使用 addOperations:waitUntilFinished: 方法一次添加一組operations,或者也可以直接使用 addOperationWithBlock: 方法添加 block 對象到queue。
- [aQueue addOperation:anOp]; // Add a single operation
- [aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
- [aQueue addOperationWithBlock:^{
- /* Do something. */
- }];
Operations添加到queue后,通常短時間內就會得到運行。但是如果存在依賴,或者Operations掛起等原因,也可能需要等待。
注意Operations添加到queue之后,絕對不要再修改Operations對象。因為Operations對象可能會在任何時候運行,因此改變依賴或數據會產生不利的影響。你只能通過 NSOperation 的方法來查看操作的狀態,是否正在運行、等待運行、已經完成等。
雖然 NSOperationQueue 類設計用於並發執行Operations,你也可以強制單個queue一次只能執行一個Operation。setMaxConcurrentOperationCount: 方法可以配置operation queue的最大並發操作數量。設為1就表示queue每次只能執行一個操作。不過operation執行的順序仍然依賴於其它因素,像操作是否准備好和優先級等。因此串行化的operation queue並不等同於Grand Central Dispatch中的串行dispatch queue。
手動執行Operations
手動執行Operation,要求Operation已經准備好,isReady返回YES,此時你才能調用start方法來執行它。isReady方法與Operations依賴是結合在一起的。
調用start而不是main來手動執行Operation,因為start在執行你的自定義代碼之前,會首先執行一些安全檢查。而且start還會產生KVO通知,以正確地支持Operations的依賴機制。start還能處理Operations已經被取消的情況,此時會拋出一個異常。
手動執行Operation對象之前,還需要調用 isConcurrent 方法,如果返回NO,你的代碼可以決定在當前線程同步執行這個Operation,或者創建一個獨立的線程以異步執行。
下面方法演示了手動執行Operation,如果這個方法返回NO,表示不能執行,你需要設置一個定時器,稍后再次調用本方法,直到這個方法返回YES,表示已經執行Operation。
- - (BOOL)performOperation:(NSOperation*)anOp
- {
- BOOL ranIt = NO;
- if ([anOp isReady] && ![anOp isCancelled])
- {
- if (![anOp isConcurrent])
- [anOp start];
- else
- [NSThread detachNewThreadSelector:@selector(start)
- toTarget:anOp withObject:nil];
- ranIt = YES;
- }
- else if ([anOp isCancelled])
- {
- // If it was canceled before it was started,
- // move the operation to the finished state.
- [self willChangeValueForKey:@"isFinished"];
- [self willChangeValueForKey:@"isExecuting"];
- executing = NO;
- finished = YES;
- [self didChangeValueForKey:@"isExecuting"];
- [self didChangeValueForKey:@"isFinished"];
- // Set ranIt to YES to prevent the operation from
- // being passed to this method again in the future.
- ranIt = YES;
- }
- return ranIt;
- }
取消Operations
一旦添加到operation queue,queue就擁有了這個對象並且不能被刪除,唯一能做的事情是取消。你可以調用Operation對象的cancel方法取消單個操作,也可以調用operation queue的 cancelAllOperations 方法取消當前queue中的所有操作。
只有你確定不再需要Operations對象時,才應該取消它。發出取消命令會將Operations對象設置為"Canceled"狀態,會阻止它被執行。由於取消也被認為是完成,依賴於它的其它Operations對象會收到適當的KVO通知,並清除依賴狀態,然后得到執行。
因此常見的做法是當發生重大事件時,一次性取消queue中的所有操作,例如應用退出或用戶請求取消操作。
等待Operations完成
為了最佳的性能,你應該盡量設計你的應用盡可能地異步操作,讓應用在操作正在執行時可以去處理其它事情。
如果創建operation的代碼需要處理operation完成后的結果,可以使用 NSOperation 的 waitUntilFinished 方法等待operation完成。通常我們應該避免編寫這樣的代碼,阻塞當前線程可能是一種簡便的解決方案,但是它引入了更多的串行代碼,限制了整個應用的並發性,同時也降低了用戶體驗。
絕對不要在應用主線程中等待一個Operation,只能在第二或次要線程中等待。阻止主線程將導致應用無法響應用戶事件,應用也將表現為無響應。
除了等待單個Operation完成,你也可以同時等待一個queue中的所有操作,使用 NSOperationQueue 的 waitUntilAllOperationsAreFinished 方法。注意在等待一個queue時,應用的其它線程仍然可以往queue中添加Operation,因此可能加長你線程的等待時間。
掛起和繼續Queue
如果你想臨時掛起Operations的執行,可以使用 setSuspended: 方法暫停相應的queue。不過掛起一個queue不會導致正在執行的Operation在任務中途暫停,只是簡單地阻止調度新Operation執行。你可以在響應用戶請求時,掛起一個queue,來暫停等待中的任務。稍后根據用戶的請求,可以再次調用 setSuspended: 方法繼續Queue中操作的執行。
Dispatch Queues
dispatch queues是執行任務的強大工具,允許你同步或異步地執行任意代碼block。原先使用單獨線程執行的所有任務都可以替換為使用dispatch queues。而dispatch queues最大的優點在於使用簡單,而且更加高效。
dispatch queues任務的概念就是應用需要執行的一些工作,如計算、創建或修改數據結構、處理數據等等。我們使用函數或block對象來定義任務,並添加到dispatch queue。
dispatch queue是類似於對象的結構體,管理你提交給它的任務,而且都是先進先出的數據結構。因此queue中的任務總是以添加的順序開始執行。Grand Central Disaptch提供了幾種dispatch queues,不過你也自己創建。
類型 | 描述 |
串行 | 也稱為private dispatch queue,每次只執行一個任務,按任務添加順序執行。當前正在執行的任務在獨立的線程中運行(不同任務的線程可能不同),dispatch queue管理了這些線程。通常串行queue主要用於對特定資源的同步訪問。 你可以創建任意數量的串行queues,雖然每個queue本身每次只能執行一個任務,但是各個queue之間是並發執行的。 |
並發 | 也稱為global dispatch queue,可以並發執行一個或多個任務,但是任務仍然是以添加到queue的順序啟動。每個任務運行於獨立的線程中,dispatch queue管理所有線程。同時運行的任務數量隨時都會變化,而且依賴於系統條件。 你不能創建並發dispatch queues。相反應用只能使用三個已經定義好的全局並發queues。 |
Main dispatch queue | 全局可用的串行queue,在應用主線程中執行任務。這個queue與應用的 run loop 交叉執行。由於它運行在應用的主線程,main queue通常用於應用的關鍵同步點。 雖然你不需要創建main dispatch queue,但你必須確保應用適當地回收 |
應用使用dispatch queue,相比線程有很多優點,最直接的優點是簡單,不用編寫線程創建和管理的代碼,讓你集中精力編寫實際工作的代碼。另外系統管理線程更加高效,並且可以動態調控所有線程。
dispatch queue比線程具有更強的可預測性,例如兩個線程訪問共享資源,你可能無法控制哪個線程先后訪問;但是把兩個任務添加到串行queue,則可以確保兩個任務對共享資源的訪問順序。同時基於queue的同步也比基於鎖的線程同步機制更加高效。
應用有效地使用dispatch queue,要求盡可能地設計自包含、可以異步執行的任務。
dispatch queues的幾個關鍵點:
dispatch queues相對其它dispatch queues並發地執行任務,串行化任務只能在同一個dispatch queue中實現。
系統決定了同時能夠執行的任務數量,應用在100個不同的queues中啟動100個任務,並不表示100個任務全部都在並發地執行(除非系統擁有100或更多個核)
系統在選擇執行哪個任務時,會考慮queue的優先級。
queue中的任務必須在任何時候都准備好運行,注意這點和Operation對象不同。
private dispatch queue是引用計數的對象。你的代碼中需要retain這些queue,另外dispatch source也可能添加到一個queue,從而增加retain的計數。因此你必須確保所有dispatch source都被取消,而且適當地調用release。
Queue相關的技術
除了dispatch queue,Grand Central Disaptch還提供幾個相關的技術,使用queue來幫助你管理代碼。
技術 | 描述 |
Dispatch group | 用於監控一組block對象完成(你可以同步或異步地監控block)。Group提供了一個非常有用的同步機制,你的代碼可以等待其它任務的完成 |
Dispatch semaphore | 類似於傳統的semaphore(信號量),但是更加高效。只有當調用線程由於信號量不可用,需要阻塞時,Dispatch semaphore才會去調用內核。如果信號量可用,就不會與內核進行交互。使用信號量可以實現對有限資源的訪問控制 |
Dispatch source | Dispatch source在特定類型的系統事件發生時,會產生通知。你可以使用dispatch source來監控各種事件,如:進程通知、信號、描述符事件、等等。當事件發生時,dispatch source異步地提交你的任務到指定的dispatch queue,來進行處理。 |
Block可以非常容易地定義“自包含”的工作單元,盡管看上去非常類似於函數指針,block實際上由底層數據結構來表示,由編譯器負責創建和管理。編譯器對你的代碼(和所有相關的數據)進行打包,封裝為可以存在於堆中的格式,並在你的應用各個地方傳遞。
Block最關鍵的優點能夠使用own lexical scope之外的變量,在函數或方法內部定義一個block時,block可以直接讀取父scope中的變量。block訪問的變量全部被拷貝到block在堆中的數據結構,這樣block就能在稍后自由地訪問這些變量。當block被添加到dispatch queue中時,這些變量通常是只讀格式的。不過同步執行的Block對象,可以使用那些定義為__block的變量,對這些變量的修改會影響到調用scope。
Block的簡單用法:
- int x = 123;
- int y = 456;
- // Block declaration and assignment
- void (^aBlock)(int) = ^(int z) {
- printf("%d %d %d\n", x, y, z);
- };
- // Execute the block
- aBlock(789); // prints: 123 456 789
設計Block時需考慮以下關鍵指導方針:
對於使用dispatch queue的異步Block,可以在Block中安全地捕獲和使用父函數或方法中的scalar變量。但是Block不應該去捕獲大型結構體或其它基於指針的變量,這些變量由Block的調用上下文分配和刪除。在你的Block被執行時,這些指針引用的內存可能已經不存在。當然,你自己顯式地分配內存(或對象),然后讓Block擁有這些內存的所有權,是安全可行的。
Dispatch queue對添加的Block會進行復制,在完成執行后自動釋放。換句話說,你不需要在添加Block到Queue時顯式地復制
盡管Queue執行小任務比原始線程更加高效,仍然存在創建Block和在Queue中執行的開銷。如果Block做的事情太少,可能直接執行比dispatch到queue更加有效。使用性能工具來確認Block的工作是否太少
絕對不要針對底層線程緩存數據,然后期望在不同Block中能夠訪問這些數據。如果相同queue中的任務需要共享數據,應該使用dispatch queue的context指針來存儲這些數據。
如果Block創建了大量Objective-C對象,考慮創建自己的autorelease pool,來處理這些對象的內存管理。雖然dispatch queue也有自己的autorelease pool,但不保證在什么時候會回收這些pool。
創建和管理Dispatch Queue
獲得全局並發Dispatch Queue
並發dispatch queue可以同時並行地執行多個任務,不過並發queue仍然按先進先出的順序來啟動任務,並發queue會在之前任務完成之前就出列下一個任務並啟動執行。並發queue同時執行的任務數量會根據應用和系統動態變化,各種因素包括:可用核數量、其它進程正在執行的工作數量、其它串行dispatch queue中優先任務的數量等。
系統給每個應用提供三個並發dispatch queue,所有應用全局共享,三個queue的區別是優先級。你不需要顯式地創建這些queue,使用 dispatch_get_global_queue 函數來獲取這三個queue:
- dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
除了默認優先級的並發queue,你還可以獲得高和低優先級的兩個,分別使用 DISPATCH_QUEUE_PRIORITY_HIGH 和 DISPATCH_QUEUE_PRIORITY_LOW 常量來調用上面函數。
雖然dispatch queue是引用計數的對象,但你不需要retain和release全局並發queue。因為這些queue對應用是全局的,retain和release調用會被忽略。
你也不需要存儲這三個queue的引用,每次都直接調用 dispatch_get_global_queue 獲得queue就行了。
創建串行Dispatch Queue
應用的任務需要按特定順序執行時,就需要使用串行Dispatch Queue,串行queue每次只能執行一個任務。你可以使用串行queue來替代鎖,保護共享資源或可變的數據結構。和鎖不一樣的是,串行queue確保任務按可預測的順序執行。而且只要你異步地提交任務到串行queue,就永遠不會產生死鎖。
你必須顯式地創建和管理所有你使用的串行queue,應用可以創建任意數量的串行queue,但不要為了同時執行更多任務而創建更多的串行queue。如果你需要並發地執行大量任務,應該把任務提交到全局並發Queue。
創建串行queue時,你需要明確自己的目的,如保護共享資源,或同步應用的某些關鍵行為。
dispatch_queue_create 函數創建串行queue,兩個參數分別是queue名和一組queue屬性。調試器和性能工具會顯示queue的名字,便於你跟蹤任務的執行。
- dispatch_queue_t queue;
- queue = dispatch_queue_create("com.example.MyQueue", NULL);
運行時獲得公共Queue
Grand Central Disaptch提供函數,讓應用訪問幾個公共dispatch queue:
使用 dispatch_get_current_queue 函數作為調試用途,或者測試當前queue的標識。在block對象中調用這個函數會返回block提交到的queue(這個時候queue應該正在執行中)。在block對象之外調用這個函數會返回應用的默認並發queue。
使用 dispatch_get_main_queue 函數獲得應用主線程關聯的串行dispatch queue。Cocoa 應用、調用了 dispatch_main 函數或配置了run loop(CFRunLoopRef 類型 或一個 NSRunLoop 對象)的應用,會自動創建這個queue。
使用 dispatch_get_global_queue 來獲得共享的並發queue
Dispatch Queue的內存管理
Dispatch Queue和其它dispatch對象都是引用計數的數據類型。當你創建一個串行dispatch queue時,初始引用計數為1,你可以使用 dispatch_retain 和 dispatch_release 函數來增加和減少引用計數。當引用計數到達0時,系統會異步地銷毀這個queue。
對dispatch對象(如queue)retain和release是很重要的,確保它們被使用時能夠保留在內存中。和內存托管的Cocoa對象一樣,通用的規則是如果你使用一個傳遞給你代碼中的queue,你應該在使用前retain,使用完之后release。
你不需要retain或release全局dispatch queue,包括全局並發 dispatch queue和main dispatch queue。
即使你實現的是自動垃圾收集的應用,也需要retain和release你的dispatch queue和其它dispatch對象。Grand Central Disaptch不支持垃圾收集模型來回收內存。
在Queue中存儲自定義上下文信息
所有dispatch對象(包括dispatch queue)都允許你關聯custom context data。使用 dispatch_set_context 和 dispatch_get_context 函數來設置和獲取對象的上下文數據。系統不會使用你的上下文數據,所以需要你自己在適當的時候分配和銷毀這些數據。
對於Queue,你可以使用上下文數據來存儲一個指針,指向Objective-C對象或其它數據結構,協助標識這個queue或代碼的其它用途。你可以使用queue的finalizer函數來銷毀(或解除關聯)上下文數據。
為Queue提供一個清理函數
在創建串行dispatch queue之后,可以附加一個finalizer函數,在queue被銷毀之前執行自定義的清理操作。使用 dispatch_set_finalizer_f 函數為queue指定一個清理函數,當queue的引用計數到達0時,就會執行該清理函數。你可以使用清理函數來解除queue關聯的上下文數據,而且只有上下文指針不為NULL時才會調用這個清理函數。
下面例子演示了自定義finalizer函數的使用,你需要自己提供 myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函數,用於初始化和清理上下文數據。
- void myFinalizerFunction(void *context)
- {
- MyDataContext* theData = (MyDataContext*)context;
- // Clean up the contents of the structure
- myCleanUpDataContextFunction(theData);
- // Now release the structure itself.
- free(theData);
- }
- dispatch_queue_t createMyQueue()
- {
- MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext));
- myInitializeDataContextFunction(data);
- // Create the queue and set the context data.
- dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
- if (serialQueue)
- {
- dispatch_set_context(serialQueue, data);
- dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
- }
- return serialQueue;
- }
添加任務到Queue
要執行一個任務,你需要將它dispatch到一個適當的dispatch queue,你可以同步或異步地dispatch一個任務,也可以單個或按組來dispatch。一旦進入到queue,queue會負責盡快地執行你的任務。
添加單個任務到Queue
你可以異步或同步地添加一個任務到Queue,盡可能地使用 dispatch_async 或 dispatch_async_f 函數異步地dispatch任務。因為添加任務到Queue中時,無法確定這些代碼什么時候能夠執行。因此異步地添加block或函數,可以讓你立即調度這些代碼的執行,然后調用線程可以繼續去做其它事情。
特別是應用主線程一定要異步地dispatch任務,這樣才能及時地響應用戶事件。
少數時候你可能希望同步地dispatch任務,以避免競爭條件或其它同步錯誤。使用 dispatch_sync 和 dispatch_sync_f 函數同步地添加任務到Queue,這兩個函數會阻塞,直到相應任務完成執行。
絕對不要在任務中調用 dispatch_sync 或 dispatch_sync_f 函數,並同步dispatch新任務到當前正在執行的queue。對於串行queue這一點特別重要,因為這樣做肯定會導致死鎖;而並發queue也應該避免這樣做。
- dispatch_queue_t myCustomQueue;
- myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);
- dispatch_async(myCustomQueue, ^{
- printf("Do some work here.\n");
- });
- printf("The first block may or may not have run.\n");
- dispatch_sync(myCustomQueue, ^{
- printf("Do some more work here.\n");
- });
- printf("Both blocks have completed.\n");
任務完成時執行Completion Block
dispatch到queue中的任務,通常與創建任務的代碼獨立運行。在任務完成時,應用可能希望得到通知並使用任務完成的結果數據。在傳統的異步編程模型中,你可能會使用回調機制,不過dispatch queue允許你使用Completion Block。
Completion Block是你dispatch到queue的另一段代碼,在原始任務完成時自動執行。調用代碼在啟動任務時通過參數提供Completion Block。任務代碼只需要在完成工作時提交指定的Block或函數到指定的queue。
下面代碼使用block實現了平均數,最后兩個參數允許調用方指定一個queue和報告結果的block。在平均數函數完成計算后,會傳遞結果到指定的block,並dispatch到指定的queue。為了防止queue被過早地釋放,必須首先retain這個queue,然后在dispatch這個Completion Block之后,再release這個queue。
- void average_async(int *data, size_t len,
- dispatch_queue_t queue, void (^block)(int))
- {
- // Retain the queue provided by the user to make
- // sure it does not disappear before the completion
- // block can be called.
- dispatch_retain(queue);
- // Do the work on the default concurrent queue and then
- // call the user-provided block with the results.
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- int avg = average(data, len);
- dispatch_async(queue, ^{ block(avg);});
- // Release the user-provided queue when done
- dispatch_release(queue);
- });
- }
並發地執行Loop Iteration
如果你使用循環執行固定次數的迭代,並發dispatch queue可能會提高性能。例如下面for循環:
- for (i = 0; i < count; i++) {
- printf("%u\n",i);
- }
如果每次迭代執行的任務與其它迭代獨立無關,而且循環迭代執行順序也無關緊要的話,你可以調用 dispatch_apply 或 dispatch_apply_f 函數來替換循環。這兩個函數為每次循環迭代將指定的block或函數提交到queue。當dispatch到並發queue時,就有可能同時執行多個循環迭代。
調用 dispatch_apply 或 dispatch_apply_f 時你可以指定串行或並發queue。並發queue允許同時執行多個循環迭代,而串行queue就沒太大必要使用了。
和普通for循環一樣,dispatch_apply 和 dispatch_apply_f 函數也是在所有迭代完成之后才會返回。因此在queue上下文執行的代碼中再次調用這兩個函數時,必須非常小心。如果你傳遞的參數是串行queue,而且正是執行當前代碼的Queue,就會產生死鎖。
另外這兩個函數還會阻塞當前線程,因此在主線程中調用這兩個函數同樣必須小心,可能會阻止事件處理循環並無法響應用戶事件。所以如果循環代碼需要一定的時間執行,你可以考慮在另一個線程中調用這兩個函數。
下面代碼使用 dispatch_apply 替換了for循環,你傳遞的block必須包含一個參數,用來標識當前循環迭代。第一次迭代這個參數值為0,第二次時為1,最后一次值為count - 1。
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_apply(count, queue, ^(size_t i) {
- printf("%u\n",i);
- });
循環迭代執行的工作量需要仔細平衡,太多的話會降低響應性;太少則會影響整體性能,因為調度的開銷大於實際執行代碼。
在主線程中執行任務
Grand Central Disaptch提供一個特殊dispatch queue,可以在應用的主線程中執行任務。應用主線程設置了run loop(由CFRunLoopRef 類型或 NSRunLoop 對象管理),就會自動創建這個queue,並且自動drain。非Cocoa應用如果不顯式地設置run loop,就必須顯式地調用dispatch_main 函數來顯式地drain這個dispatch queue。否則雖然你可以添加任務到queue,但任務永遠不會被執行。
調用 dispatch_get_main_queue 函數獲得應用主線程的dispatch queue。添加到這個queue的任務由主線程串行化執行,因此你可以在應用的某些地方使用這個queue作為同步點。
任務中使用Objective-C對象
Grand Central Disaptch支持Cocoa內存管理機制,因此可以在提交到queue的block中自由地使用Objective-C對象。每個dispatch queue維護自己的autorelease pool確保釋放autorelease對象,但是queue不保證這些對象實際釋放的時間。在自動垃圾收集的應用中,Grand Central Disaptch會在垃圾收集系統中注冊自己創建的每個線程。
如果應用消耗大量內存,並且創建大量autorelease對象,你需要創建自己的autorelease pool,用來及時地釋放不再使用的對象。
掛起和繼續queue
我們可以暫停一個queue以阻止它執行block對象,使用 dispatch_suspend 函數掛起一個dispatch queue;使用 dispatch_resume 函數繼續dispatch queue。調用 dispatch_suspend 會增加queue的引用計數,調用 dispatch_resume 則減少queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。因此你必須對應地調用suspend和resume函數。
掛起和繼續是異步的,而且只在執行block之間生效。掛起一個queue不會導致正在執行的block停止。
使用Dispatch Semaphore控制有限資源的使用
如果提交到dispatch queue中的任務需要訪問某些有限資源,可以使用dispatch semaphore來控制同時訪問這個資源的任務數量。dispatch semaphore和普通的信號量類似,唯一的區別是當資源可用時,需要更少的時間來獲得dispatch semaphore。
使用dispatch semaphore的過程如下:
使用 dispatch_semaphore_create 函數創建semaphore,指定正數值表示資源的可用數量。
在每個任務中,調用 dispatch_semaphore_wait 來等待Semaphore
當上面調用返回時,獲得資源並開始工作
使用完資源后,調用 dispatch_semaphore_signal 函數釋放和signal這個semaphore
- // Create the semaphore, specifying the initial pool size
- dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);
- // Wait for a free file descriptor
- dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
- fd = open("/etc/services", O_RDONLY);
- // Release the file descriptor when done
- close(fd);
- dispatch_semaphore_signal(fd_sema);
等待queue中的一組任務
Dispatch group用來阻塞一個線程,直到一個或多個任務完成執行。有時候你必須等待任務完成的結果,然后才能繼續后面的處理。dispatch group也可以替代線程join。
基本的流程是設置一個組,dispatch任務到queue,然后等待結果。你需要使用 dispatch_group_async 函數,會關聯任務到相關的組和queue。使用 dispatch_group_wait 等待一組任務完成。
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_group_t group = dispatch_group_create();
- // Add a task to the group
- dispatch_group_async(group, queue, ^{
- // Some asynchronous work
- });
- // Do some other work while the tasks execute.
- // When you cannot make any more forward progress,
- // wait on the group to block the current thread.
- dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
- // Release the group when it is no longer needed.
- dispatch_release(group);
Dispatch Queue和線程安全性
使用Dispatch Queue實現應用並發時,也需要注意線程安全性:
Dispatch queue本身是線程安全的。換句話說,你可以在應用的任意線程中提交任務到dispatch queue,不需要使用鎖或其它同步機制。
不要在執行任務代碼中調用 dispatch_sync 函數調度相同的queue,這樣做會死鎖這個queue。如果你需要dispatch到當前queue,需要使用 dispatch_async 函數異步調度
避免在提交到dispatch queue的任務中獲得鎖,雖然在任務中使用鎖是安全的,但在請求鎖時,如果鎖不可用,可能會完全阻塞串行queue。類似的,並發queue等待鎖也可能阻止其它任務的執行。如果代碼需要同步,就使用串行dispatch queue。
雖然可以獲得運行任務的底層線程的信息,最好不要這樣做。
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繼續。但是不會遞送所有事件,而是先合並到單一事件,然后再一次遞送。例如你監控一個文件的文件名變化,就只會遞送最后一次的變化事件。
Migrating Away from Threads
從現有的線程代碼遷移到Grand Central Dispatch和Operation對象有許多方法,盡管可能不是所有線程代碼都能夠執行遷移,但是遷移可能提升性能,並簡化你的代碼。
使用dispatch queue和Operaiton queue相比線程擁有許多優點:
應用不再需要存儲線程棧到內存空間
消除了創建和配置線程的代碼
消除了管理和調度線程工作的代碼
簡化了你要編寫的代碼
使用Dispatch Queue替代線程
首先考慮應用可能使用線程的幾種方式:
單一任務線程:創建一個線程執行單一任務,任務完成時釋放線程
工作線程(Worker):創建一個或多個工作線程執行特定的任務,定期地分配任務給每個線程
線程池:創建一個通用的線程池,並為每個線程設置run loop,當你需要執行一個任務時,從池中抓取一個線程,並分配任務給它。如果沒有空閑線程可用,任務進入等待隊列。
雖然這些看上去是完全不同的技術,但實際上只是相同原理的變種。應用都是使用線程來執行某些任務,區別在於管理線程和任務排隊的代碼。使用dispatch queue和operation queue,你可以消除所有線程、及線程通信的代碼,集中精力編寫處理任務的代碼。
如果你使用了上面的線程模型,你應該已經非常了解應用需要執行的任務類型,只需要封裝任務到Operation對象或Block對象,然后dispatch到適當的queue,就一切搞定!
對於那些不使用鎖的任務,你可以直接使用以下方法來進行遷移:
單一任務線程,封裝任務到block或operation對象,並提交到並發queue
工作線程,首先你需要確定使用串行queue還是並發queue,如果工作線程需要同步特定任務的執行,就應該使用串行queue。如果工作線程只是執行任意任務,任務之間並無關聯,就應該使用並發queue
線程池,封裝任務到block或operation對象,並提交到並發queue中執行
當然,上面只是簡單的情況。如果任務會爭奪共享資源,理想的解決方案當然是消除或最小化共享資源的爭奪。如果有辦法重構代碼,消除任務彼此對共享資源的依賴,這是最理想的。
如果做不到消除共享資源依賴,你仍然可以使用queue,因為queue能夠提供可預測的代碼執行順序。可預測意味着你不需要鎖或其它重量級的同步機制,就可以實現代碼的同步執行。
你可以使用queue來取代鎖執行以下任務:
如果任務必須按特定順序執行,提交到串行dispatch queue;如果你想使用Operation queue,就使用Operation對象依賴來確保這些對象的執行順序。
如果你已經使用鎖來保護共享資源,創建一個串行queue來執行任務並修改該資源。串行queue可以替換現有的鎖,直接作為同步機制使用。
如果你使用線程join來等待后台任務完成,考慮使用dispatch group;也可以使用一個 NSBlockOperation 對象,或者Operation對象依賴,同樣可以達到group-completion的行為。
如果你使用“生產者-消費者”模型來管理有限資源池,考慮使用 dispatch queue 來簡化“生產者-消費者”
如果你使用線程來讀取和寫入描述符,或者監控文件操作,使用dispatch source
記住queue不是替代線程的萬能葯!queue提供的異步編程模型適合於延遲無關緊要的場合。雖然queue提供配置任務執行優先級的方法,但更高的優先級也不能確保任務一定能在特定時間得到執行。因此線程仍然是實現最小延遲的適當選擇,例如音頻和視頻playback等場合。
消除基於鎖的代碼
在線程代碼中,鎖是傳統的多個線程之間同步資源的訪問機制。但是鎖的開銷本身比較大,線程還需等待鎖的釋放。
使用queue替代基於鎖的線程代碼,消除了鎖帶來的開銷,並且簡化了代碼編寫。你可以將任務放到串行queue,來控制任務對共享資源的訪問。queue的開銷要遠遠小於鎖,因為將任務放入queue不需要陷入內核來獲得mutex
將任務放入queue時,你做的主要決定是同步還是異步,異步提交任務到queue讓當前線程繼續運行;同步提交任務則阻塞當前線程,直到任務執行完成。兩種機制各有各的用途,不過通常異步優先於同步。
實現異步鎖
異步鎖可以保護共享資源,而又不阻塞任何修改資源的代碼。當代碼的部分工作需要修改一個數據結構時,可以使用異步鎖。使用傳統的線程,你的實現方式是:獲得共享資源的鎖,做必要的修改,釋放鎖,繼續任務的其它部分工作。但是使用dispatch queue,調用代碼可以異步修改,無需等待這些修改操作完成。
下面是異步鎖實現的一個例子,受保護的資源定義了自己的串行dispatch queue。調用代碼提交一個block到這個queue,在block中執行對資源的修改。由於queue串行的執行所有block,對這個資源的修改可以確保按順序進行;而且由於任務是異步執行的,調用線程不會阻塞。
dispatch_async(obj->serial_queue, ^{
// Critical section
});
同步執行臨界區
如果當前代碼必須等到指定任務完成,你可以使用 dispatch_sync 函數同步的提交任務,這個函數將任務添加到dispatch queue,並阻塞當前線程直到任務完成執行。dispatch queue本身可以是串行或並發queue,你可以根據具體的需要來選擇使用。由於 dispatch_sync 函數會阻塞當前線程,你只應該在確實需要的時候才使用。
下面是使用 dispatch_sync 實現臨界區的例子:
dispatch_sync(my_queue, ^{
// Critical section
});
如果你已經使用串行queue保護一個共享資源,同步提交到串行queue,並不能比異步提交提供更多的保護。同步提交的唯一理由是,阻止當前代碼在臨界區完成之前繼續執行。如果當前代碼不需要等待臨界區完成,或者可以簡單的提交接下來的任務到相同的串行queue,就應該使用異步提交。
改進循環代碼
如果循環每次迭代執行的工作互相獨立,可以考慮使用 dispatch_apply 或 dispatch_apply_f 函數來重新實現循環。這兩個函數將循環的每次迭代提交到dispatch queue進行處理。結合並發queue使用時,可以並發地執行迭代以提高性能。
dispatch_apply 和 dispatch_apply_f 是同步函數,會阻塞當前線程直到所有循環迭代執行完成。當提交到並發queue時,循環迭代的執行順序是不確定的。因此你用來執行循環迭代的Block對象(或函數)必須可重入(reentrant)。
下面例子使用dispatch來替換循環,你傳遞給 dispatch_apply 或 dispatch_apply_f 的Block或函數必須有一個整數參數,用來標識當前的循環迭代:
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n", i);
});
你需要明智地使用這項技術,因為dispatch queue的開銷雖然非常小,但仍然存在,你的循環代碼必須擁有足夠的工作量,才能忽略掉dispatch queue的這些開銷。
提升每次循環迭代工作量最簡單的辦法是striding(跨步),重寫block代碼執行多個循環迭代。從而減少了 dispatch_apply 函數指定的count值。
int stride = 137;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count / stride, queue, ^(size_t idx){
size_t j = idx * stride;
size_t j_stop = j + stride;
do {
printf("%u\n", (unsigned int)j++);
}while (j < j_stop);
});
// 執行剩余的循環迭代
size_t i;
for (i = count - (count % stride); i < count; i++)
printf("%u\n", (unsigned int)i);
如果循環迭代次數非常多,使用stride可以提升性能。
替換線程Join
線程join允許你生成多個線程,然后讓當前線程等待所有線程完成。線程創建子線程時指定為joinable,如果父線程在子線程完成之前不能繼續處理,就可以join子線程。join會阻塞父線程直到子線程完成任務並退出,這時候父線程可以獲得子線程的結果狀態,並繼續自己的工作。父線程可以一次性join多個子線程。
Dispatch Group提供了類似於線程join的語義,但擁有更多優點。dispatch group可以讓線程阻塞直到一個或多個任務完成。和線程join不一樣的是,dispatch goup同時等待所有子任務完成。而且由於dispatch group使用dispatch queue來執行任務,更加高效。
以下步驟可以使用dispatch group替換線程join:
使用 dispatch_group_create 函數創建一個新的dispatch group
使用 dispatch_group_async 或 dispatch_group_async_f 函數添加任務到Group,這些是你要等待完成的任務
如果當前線程不能繼續處理任何工作,調用 dispatch_group_wait 函數等待這個group,會阻塞當前線程直到group中的所有任務執行完成。
如果你使用Operation對象來實現任務,可以使用依賴來實現線程join。不過這時候不是讓父線程等待所有任務完成,而是將父代碼移到一個Operation對象,然后設置父Operation對象依賴於所有子Operation對象。這樣父Operation對象就會等到所有子Operation執行完成后才開始執行。
修改“生產者-消費者”實現
生產者-消費者 模型可以管理有限數量動態生產的資源。生產者生成新資源,消費者等待並消耗這些資源。實現生產者-消費者模型的典型機制是條件或信號量。
使用條件(Condition)時,生產者線程通常如下:
鎖住與condition關聯的mutex(使用pthread_mutex_lock)
生產資源(或工作)
Signal條件變量,通知有資源(或工作)可以消費(使用pthread_cond_signal)
解鎖mutex(使用pthread_mutex_unlock)
對應的消費者線程則如下:
鎖住condition關聯的mutex(使用pthread_mutex_lock)
設置一個while循環[list=1]
檢查是否有資源(或工作)
如果沒有資源(或工作),調用pthread_cond_wait阻塞當前線程,直到相應的condition觸發
獲得生產者提供的資源(或工作)解鎖mutex(使用pthread_mutex_unlock)處理資源(或工作)使用dispatch queue,你可以簡化生產者-消費者為一個調用:
dispatch_async(queue, ^{
// Process a work item.
});
當生產者有工作需要做時,只需要將工作添加到queue,並讓queue去處理該工作。唯一需要確定的是queue的類型,如果生產者生成的任務需要按特定順序執行,就使用串行queue;否則使用並發Queue,讓系統盡可能多地同時執行任務。
替換Semaphore代碼
使用信號量可以限制對共享資源的訪問,你應該考慮使用dispatch semaphore來替換普通信號量。傳統的信號量需要陷入內核,而dispatch semaphore可以在用戶空間快速地測試狀態,只有測試失敗調用線程需要阻塞時才會陷入內核。這樣dispatch semaphore擁有比傳統semaphore快得多的性能。兩者的行為是一致的。
替換Run-Loop代碼
如果你使用run loop來管理一個或多個線程執行的工作,你會發現使用queue來實現和維護任務會簡單許多。設置自定義run loop需要同時設置底層線程和run loop本身。run-loop代碼則需要設置一個或多個run loop source,並編寫回調來處理這些source事件到達。你可以創建一個串行queue,並dispatch任務到queue中,這樣一行代碼就能夠替換原有的run-loop創建代碼:
dispatch_queue_t myNewRunLoop = dispatch_queue_create("com.apple.MyQueue", NULL);
由於queue自動執行添加進來的任務,不需要編寫額外的代碼來管理queue。你也不需要創建和配置線程,更不需要創建或附加任何run-loop source。此外,你可以通過簡單地添加任務就能讓queue執行其它類型的任務,而run loop要實現這一點,必須修改現有run loop source,或者創建一個新的run loop source。
run loop的一個常用配置是處理網絡socket異步到達的數據,現在你可以附加dispatch source到需要的queue中,來實現這個行為。dispatch source還能提供更多處理數據的選項,支持更多類型的系統事件處理。
與POSIX線程的兼容性
Grand Central Dispatch管理了任務和運行線程之間的關系,通常你應該避免在任務代碼中使用POSIX線程函數,如果一定要使用,請小心。
應用不能刪除或mutate不是自己創建的數據結構。使用dispatch queue執行的block對象不能調用以下函數:
pthread_detach
pthread_cancel
pthread_join
pthread_kill
pthread_exit
任務運行時修改線程狀態是可以的,但你必須還原線程原來的狀態。只要你記得還原線程的狀態,下面函數是安全的:
pthread_setcancelstate
pthread_setcanceltype
pthread_setschedparam
pthread_sigmask
pthread_setspecific
特定block的執行線程可以在多次調用間會發生變化,因此應用不應該依賴於以下函數返回的信息:
pthread_self
pthread_getschedparam
pthread_get_stacksize_np
pthread_get_stackaddr_np
pthread_mach_thread_np
pthread_from_mach_thread_np
pthread_getspecific
Block必須捕獲和禁止任何語言級的異常,Block執行期間的其它錯誤也應該由block處理,或者通知應用