GCD調度隊列是執行任務的強大工具。調度隊列允許您相對于調度者異步或者同步的執行任意代碼塊。您能夠使用調度隊列來執行幾乎所有在單獨線程上執行的任務。調度隊列的優點是它們比線程代碼更簡單且更高效。
下面提供了調度隊列的簡介,以及在應用程序中怎么使用調度隊列執行一般的任務。如果您想用使用調度隊列替換已經存在的線程代碼,請參閱線程遷移。
關于調度隊列
調度隊列是在應用程序中異步并發執行任務的一種簡單方法。任務通常是應用程序需要執行的一些工作。例如,您可能定義一個任務來執行一些計算,創建和修改數據結構,處理從文件中讀取的數據,或者任意數量的事情。通過放置相應的代碼到函數或者塊對象中來定義任務,并把他們放到調度隊列中。
調度隊列是一個類似對象的結構體,它管理您提交給它的任務。所有的調度隊列都是先進先出的數據結構。因此,添加到隊列的任務始終以添加他們的相同順序開始執行。GCD自動為您提供一些調度隊列,但您可以為特定目的創建其他隊列。下表列出了應用程序中可以使用的調度隊列以及怎么使用它們。
類型 | 描述 |
---|---|
串行 | 串行隊列(也稱為私有調度隊列)以添加它們到隊列的順序每次執行一個任務。當前執行的任務運行在一個被調度隊列管理的不同線程上(可以隨任務變化)。串行隊列經常用于同步訪問特殊資源。 您可以根據需要創建盡可能多的串行隊列,每個隊列相對于其他隊列并行運行。也就是說,如果你創建四個串行隊列,每個隊列同一時間只執行一個任務,但是仍然可以有多達四個任務同時執行,每個隊列一個。有關怎么創建串行隊列,請參閱創建串行調度隊列。 |
并行 | 并發隊列(也稱為一類全局調度隊列)同時執行一個或者多個任務,但任務仍然以被添加到隊列的順序開始執行。當前執行的任務運行在被調度隊列管理的不同線程上。在給定的時間點執行的任務數量是可變的,并且取決于系統調節。在iOS5及以后,你可以通過指定隊列類型為DISPATCH_QUEUE_CONCURRENT來自己創建并發隊列。此外,還有四個預定義的全局并發隊列供應用程序使用。有關怎么回去全局并發隊列,請參閱獲取全局并發隊列。 |
主調度隊列 | 主調隊隊列是一個全局可用的串行隊列,它在應用程序的主線程上執行任務。這個隊列與應用程序的RunLoop(如果存在)交錯處理排隊的任務以及添加到RunLoop的其他事件源。因為它運行在程序的主線程上,所以主隊列經常作為應用程序的關鍵同步點。雖然您不需要創建主調度隊列,但您需要確保您的應用程序適當的釋放它。有關如何管理此隊列,請參閱在主線程中執行任務。 |
當向應用程序添加并發時,調度隊列提供了優于線程的幾個優點。最直接的優點是工作隊列編程的簡單性。使用線程,您必須編寫執行的工作以及創建和管理線程的代碼。調度隊列使您專注于您實際想要執行的工作,而不用擔心線程的創建和管理。相反,系統會為您處理所有的線程創建和管理。優點是,系統能夠比任何單個應用更高效的管理線程。系統可以根據可用資源和當前系統的情況動態調整線程數量。另外,系統通常能夠比您自己創線程更快的開始運行您的任務。
雖然您可能認為編寫調度隊列代碼可能是困難的,但是通常編寫調度隊列比編寫線程更簡單。編碼的關鍵是設計獨立的且可以異步運行的任務。(這實際上對線程和調度隊列都是真的。)但是調度隊列有可預見性的優點。如果您有兩個任務來訪問相同的共享資源,但是運行在不同的線程上,每個線程都可以首先修改資源,您可能需要使用鎖,以確保這兩個任務不能同時修改該資源。使用調度隊列,您可以添加兩個任務到一個串行隊列,以確保在任何給定時間只有一個任務修改資源。這種基于隊列的同步比鎖更高效,因為鎖在有競爭和無競爭的情況下總是需要一個昂貴的內核陷阱,而調度隊列主要在應用程序的進程空間中工作,只有在絕對必要時才調用內核。
雖然您可能指出,串行隊列中的兩個任務不是并發運行,但您必須記住,如果兩個線程同時使用鎖,線程提供的任何并發都會丟失或者顯著減少。更重要的,線程模型需要創建兩個線程,這兩個線程都占用內核和用戶內存空間。調度隊列不需要為他們的線程支付相同的內存損失,并且使用的線程保持忙碌且不被阻塞。
謹記以下關于調度隊列的一些其他關鍵點:
- 調度隊列相對于其他調度隊列并發執行任務。任務的串行限于單個調度隊列中的任務。
- 在任何時候系統決定執行任務的數量。因此,在100個不同隊列中有100個任務的應用程序可能不會同時執行這些任務(除非它具有100個或者更多有效的內核)。
- 在選擇要啟動的新任務時,系統會考慮隊列優先級。有關如何設置串行隊列的優先級,請參閱為隊列提供清理功能。
- 當任務被添加到隊列時,任務必須準備好執行。(如果您之前使用過Cocoa操作對象,請注意此行為與操作使用的模型不同)。
- 私有調度隊列是引用計數對象。除了在您自己的代碼中保留隊列之外,請注意調度源也可以附加到隊列,并且增加其保留計數。因此,您必須確保所有調度源都被取消,并且所有的保留調用(retain call)都通過適當的釋放調用(release call)進行平衡。有關保留和釋放隊列,請參閱調度隊列的內存管理。有關調度源的更多信息,請參閱關于調度源。
有關操作調度隊列的接口,請參閱大中央調度(GCD)參考。
隊列相關技術
除了調度隊列,GCD提供了幾種使用隊列來幫助管理代碼的技術。下表列出了這些技術,并提供了找到關于它們更多信息的鏈接。
技術 | 描述 |
---|---|
Dispatch Group | 調度組是一種用來監視一組塊對象完成的方法(您可以根據需要同步或者異步監視)。組為依賴于其他任務完成的代碼提供一種有用的同步機制。更多有關使用組的信息,請參閱等待排隊任務組。 |
Dispatch semaphores | 調度信號量類似于傳統的信號量,但通常更高效。只有當調用線程需要被阻塞時,調度信號量才調用內核,因為信號量不可用。如果信號量可用,則不進行內核調用。有關如何使用調度信號量的例子,請參閱使用調度信號量來調節有限資源的使用。 |
Dispatch sources | 調度源生成通知以響應特定類型的系統事件。您可以使用調度源來監視事件,例如進程通信,信號和描述符事件等。當事件發生時,調度源異步的將您的任務代碼提交到指定的調度隊列進行處理。有關創建和使用調度源的更多信息,請參閱調度源。 |
使用塊實現任務
塊對象(Block Object)是基于C語言的功能,可以使用C,Objective-C和C++代碼。塊使定義獨立的工作單元變的簡單。雖然他們可能看起來類似函數指針,但塊實際上是底層數據結構的表現,類似于對象,由編譯器創建和管理。編譯器將您提供的代碼(以及任何相關數據)打包,并將其封裝成可以存在于堆中并傳遞給應用程序的形式。
塊的一個關鍵優點是它們能夠使用自己的詞匯作用域之外的變量。當您在函數或者方法中定義塊時,塊在某些方法充當傳統代碼塊。例如,塊可以讀取定義在父作用域的變量值。由塊訪問的變量將被復制到堆上的塊數據結構中,因此塊可以稍后訪問它們。當塊被添加到調度隊列時,這些值通常必須以只讀格式保留。然而,被同步執行的的塊也可以使用具有__block
關鍵字的變量來返回數據到父作用域。
使用類似于函數指針語法的代碼聲明內聯塊。塊和函數指針最大的不同是,在塊名字之前使用脫字符(^)代替星號(*)。像函數指針一樣,可以傳遞參數給塊,從其接收返回值。下面代碼展示如何聲明和同步執行塊。變量aBlock
被聲明為塊,接收一個整型參數,沒返回值。然后將與該原型匹配的實際塊分配給aBlock
并聲明為內聯。最后一行立即執行塊,將指定證書打印到標準輸出。
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
下面是設計塊時需要注意的一些主要指南的摘要:
- 對于打算使用調度隊列異步執行的塊,可以安全的從父函數或者方法中獲取標量變量并在塊中使用它們。然而,不應該試圖獲取由調用上下文分配和刪除的大型結構或者其他基于指針的變量。當塊執行時,被該指針引用的內存可能消失。當然,可以自己分配內存(或者對象)并明確的將該內存的所有權交給塊。
- 調度隊列復制添加給它們的塊,并且當它們結束執行時釋放塊。換句話說,在添加它們到隊列之前,您不需要顯式的復制塊。
- 雖然隊列在執行小任務時比原始線程更高效,但仍然有創建塊和在隊列上執行它們的開銷。如果塊的工作太少,內聯的執行可能比調度到隊列成本更低。判斷塊是否工作太少的方法是使用性能工具收集每個路徑的指標,然后進行比較。
- 不要緩存和底層線程相關的數據,并希望從不同的塊訪問數據。如果同一隊列中的任務需要共享數據,使用調度隊列的上下文指針來存儲數據。 有關如何訪問調度隊列的上下文數據,請參閱使用隊列存儲自定義上下文信息 。
- 如果隊列創建多個Objective-C對象,則可能需要將塊代碼的一部分包含在@autorelease塊中,以處理這些對象的內存管理。 雖然GCD調度隊列具有自己的自動釋放池,但它們不能保證何時drain這些池。 如果您的應用程序受內存限制,創建自己的自動釋放池允許您以定期的時間間隔釋放自動釋放對象的內存。
有關塊的更多信息,包括如何聲明和使用它們,請參閱塊編程。有關怎么添加塊到調度隊列,請參閱添加任務到隊列。
創建和管理調度隊列
在將任務添加到隊列之前,必須確定要使用的隊列類型以及如何使用它。調度隊列可以串行或并發執行任務。此外,如果您對隊列有特殊用途,您可以相應地配置隊列屬性。 以下各節介紹如何創建調度隊列并對其進行配置。
獲得全局并發調度隊列
當有多個任務并行運行時,并發調度隊列很有用。并發隊列仍然是一個隊列,它以先進先出的順序對任務進行出隊,然而,在前面任何任務結束之前并發隊列可能出隊另外的任務。在任何給定時刻,并發隊列執行任務的實際數量是可變的,并且隨應用程序情況的變化而變化。許多因素影響并發隊列執行的任務數,包括可用核心數,其他進程正在完成的工作量,其他串行調度隊列中任務數量和優先級。
系統為每個應用程序提供四個并發調度隊列。這些隊列對應用程序是全局的,并且僅通過優先級來區分。因為它們是全局的,所以不需要顯式的創建它們。相反,使用dispatch_get_global_queue
函數來獲取其中一個隊列,如下所示:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
除了獲取默認并發隊列,您可以通過傳入DISPATCH_QUEUE_PRIORITY_HIGH
和DISPATCH_QUEUE_PRIORITY_LOW
常量到函數來獲取高優先級和低優先級的隊列,或者傳入DISPATCH_QUEUE_PRIORITY_BACKGROUND
常量來獲取后臺隊列。正如您所期望的,高優先級并發隊列中的任務在默認優先級和低優先級隊列中的任務之前執行。類似的,默認隊列中的任務在低優先級隊列中的任務之前執行。
重要提示:傳入
dispatch_get_global_queue
函數的第二個參數是為將來擴展保留的。現在,您應該總是為此參數傳0.
雖然調度隊列是引用計數對象,但您不需要保留和釋放全局并發隊列。因為它們對于應用程序來說是全局的,所以忽略這些隊列的保留和釋放調用。因此,您不需要保存對這些隊列的引用。任何時候您需要引用他們中的一個,只需要調用dispatch_get_global_queue
函數。
創建串行調度隊列
當想要任務按照特定的順序執行時,串行隊列非常有用。串行隊列每次只執行一個任務,并且總是從隊列首獲取任務。您可以使用串行隊列代替鎖來保護共享資源或者可變數據結構。與鎖不同的是,串行隊列能夠確保任務按照可預見的順序執行。只要以異步方式提交任務到串行隊列,隊列就永遠不會死鎖。
與已經為您創建好的并發隊列不同,您必須顯式的創建和管理任何您想要使用的串行隊列。您可以為您的應用程序創建任意數量的串行隊列,但應避免創建大量的串行隊列來盡可能多的同時執行任務。如果您想同時執行大量任務,提交他們到全局并發隊列。當創建串行隊列時,請確定每個隊列的用途,例如保護資源或者同步應用程序的某些關鍵行為。
下面代碼顯示了創建自定義串行隊列所需的步驟。dispatch_queue_create
函數有兩個參數:隊列名稱和一組隊列屬性。調試器和性能工具顯示隊列名稱,幫助您跟蹤任務如何執行。隊列屬性是為將來使用預留的,應該總是NULL。
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
除了您創建的自定義隊列,系統自動創建串行隊列并將其綁定到您應用程序的主線程。有關獲取主線程隊列的更多信息,請參閱在運行時獲取常見隊列。
注意:
dispatch_queue_create
函數第二個參數隊列屬性,傳入參數來決定隊列類型:
- DISPATCH_QUEUE_SERIAL,串行隊列,也就是NULL,
- DISPATCH_QUEUE_SERIAL_INACTIVE,也是串行隊列,不活躍的(調度隊列可能以一種不活躍的狀態被創建,在這種狀態下的隊列,隊列中blocks 被調用之前,隊列必須被激活。調用dispatch_activate函數是隊列活躍。)
- DISPATCH_QUEUE_CONCURRENT,并發隊列
- DISPATCH_QUEUE_CONCURRENT_INACTIVE,并發隊列,不活躍的
在運行時獲取常見隊列
GCD提供函數允許您從應用程序中訪問幾個常見的調度隊列:
- 使用
dispatch_get_current_queue
函數調試或者測試當前隊列的標示。在塊對象內部調用這個函數,返回塊被提交到的隊列(并且現在可能正在運行)。在塊外部調用此函數,將返回應用程序的默認并發隊列。 - 使用
dispatch_get_main_queue
函數獲取關聯到應用程序主線程的串行調度隊列。對于Cocoa應用程序和調用dispatch_main
函數或在主線程上配置RunLoop(使用CFRunLoopRef
類型或者NSRunLoop
對象)的應用程序,此隊列自動被創建。 - 使用
dispatch_get_global_queue
函數獲取任意共享的并發隊列。更多信息,請參閱獲得全局并發調度隊列。
調度隊列的內存管理
調度隊列和其他調度對象是引用計數的數據類型。當創建串行調度隊列時,他初始引用計數為1。可以根據需要使用dispatch_retain
和dispatch_release
函數來增加和減少引用計數。當隊列的引用計數為0時,系統異步的釋放(dealloc)隊列。
保留(retain)和釋放(release)調度對象(如隊列)非常重要,以確保它們在被使用時保留在內存中。與Cocoa對象的內存管理一樣,基本規則是,如果您打算使用傳遞給您代碼的隊列,在使用之前應當保留隊列,在不再需要時釋放隊列。這個基本模式確保只要您使用隊列,它就在內存中。
重要提示:您不需要保留或釋放任何全局調度隊列,包括并發調度隊列或主調度隊列。任何保留和釋放這些隊列的試圖都將被忽略。
即使您實現一個垃圾回收的應用程序,您也必須保留和釋放您的調度隊列和其他調度對象。GCD不支持用于回收內存的垃圾回收模型。
使用隊列存儲自定義上下文信息
所有調度對象(包括調度隊列)允許您將自定義上下文數據與調度對象關聯。要在調度對象上設置和獲取這些數據,可以使用dispatch_set_context
和dispatch_get_context
函數。系統不會以任何方式使用您的自定義數據,并且由您決定在適當的時候分配和釋放數據。
對于隊列,您可以使用上下文數據存儲指向Objective-C對象的指針或者其他數據結構,用來幫助標示隊列或者對代碼的預期用途。您可以在隊列釋放之前使用隊列的finalizer(終結器/清理器)函數將上下文數據從隊列中釋放(或者取消關聯)。有關如何寫finalizer函數來清理隊列的上下文數據,請參閱為隊列提供清理功能。
為隊列提供清理功能
創建串行調度隊列后,您可以附加一個finalizer函數,當隊列釋放時執行任意自定義清理。調度隊列是引用計數對象,您可以使用dispatch_set_finalizer_f
函數來指定一個函數,當隊列的引用計數為0時執行。可以使用這個函數來清理關聯到隊列的上下文數據,只要上下文指針不為NULL,這個函數就被調用。
下面代碼展示了一個自定義finalizer函數和一個創建隊列并設置finalizer的函數。隊列使用finalizer函數釋放存儲在隊列上下文指針中的數據。(代碼中的myInitializeDataContextFunction
和myCleanUpDataContextFunction
函數是自定義函數,提供初始化和清理數據結構內容功能。)傳遞給finalizer函數的上下文指針包含關聯到隊列的數據對象。
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;
}
添加任務到隊列
為了執行任務,必須將其分配到適當的調度隊列。可以同步或者異步調度任務,而且可以逐一或分組的調度它們。一旦進入隊列,隊列負責盡快執行任務,考慮它們的約束和隊列中已經存在的任務。下面介紹一些將任務分配到隊列的技術和它們的優點。
添加單個任務到隊列
有兩種方式添加任務到隊列:異步和同步。如果有可能的話,使用dispatch_async
和dispatch_async_f
函數異步執行優先于同步執行。當添加一個塊對象或函數到隊列,沒有辦法知道代碼什么時候執行。因此,異步添加塊或函數允許您調度代碼執行并且繼續在調用線程中做其他工作。如果從應用程序的主線程調度任務(可能響應一些用戶事件),這一點尤其重要。
雖然應該盡可能異步添加任務,但可能仍需要同步添加任務以防止競爭條件或者其他同步錯誤。在這些情況下,可以使用dispatch_sync
和dispatch_sync_f
函數添加任務到隊列。這些函數阻塞當前線程執行,直到指定的任務執行結束。
重要提示:永遠不要從隊列中執行的任務里調用
dispatch_sync
或dispatch_sync_f
函數 ,且傳遞給函數同一個隊列。這對的串行隊列非常重要,它產生了死鎖,對于并發隊列也應該避免。
下面示例展示如何使用基于塊的變量異步和同步調度任務:
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");
任務完成時執行完成塊
根據其性質,被調度到隊列的任務獨立于創建他們的代碼運行。然而,當任務完成時,應用程序可能希望被通知該情況,以便它可以合并結果。對于傳統異步編程,可能使用回調機制來做,但對于調度隊列,可以使用完成塊。
完成塊是在原始任務結束時調度到隊列的另外一段代碼。當任務開始時,調用代碼通常提供完成塊作為參數。任務代碼需要做的是,當它結束時,提交指定塊或者函數到指定隊列。
下面代碼展示一個使用塊實現求平均值的函數。函數的最后兩個參數允許調用者指定隊列和當匯報結果時用的塊。求平均值函數計算其結果后,傳遞結果到指定的塊并調度塊到隊列。為了防止隊列過早的被釋放,在最開始保留隊列并且在完成塊被調度后釋放隊列是至關重要的。
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);
});
}
并發執行循環迭代
在循環執行固定數量迭代的地方,并發調度隊列可能提高其性能。例如,假設有個for循環,通過每個循環迭代做一些工作:
for (i = 0; i < count; i++) {
printf("%u\n",i);
}
如果每個迭代中執行的工作與所有其他迭代中執行的工作不同,且循環完成的順序不重要。可以使用調用dispatch_apply
或dispatch_apply_f
函數來替換循環。這個函數為每次循環迭代提交指定塊或函數到隊列。當被調度到并發隊列時,因此可以同時執行多個循環迭代。
當調用dispatch_apply
或dispatch_apply_f
函數時,可以指定一個串行隊列或者并發隊列。傳入并發隊列允許您同時執行多個循環迭代,是使用這個函數最常見的方法。雖然使用串行隊列是允許的,并為您的代碼做正確的事情,但使用這樣的隊列代替循環并沒有真正的性能優勢。
重要提示:像普通循環一樣,
dispatch_apply
或dispatch_apply_f
函數不返回,直到所有循環迭代結束。因此,當從隊列上下文已經執行的代碼中調用它們時,應當小心。如果作為參數傳遞給函數的隊列是串行隊列,且與執行當前代碼的隊列是同一個隊列,調用這個方法將會使隊列死鎖。因為它們直接阻塞當前線程,所以當從主線程調用這些函數時也應當小心,它們可能阻止事件處理循環及時響應事件。如果您的代碼需要大量的處理時間,您可能需要從不同的線程調用這些函數。
下面代碼顯示如何使用dispath_apply
語法替換前面描述的for循環。傳入到dispath_apply
函數的塊必須包含一個標示當前循環迭代的參數。當塊執行時,第一次迭代參數為0,第二次為1,等等。最后一次迭代,參數的值為count - 1,count代表迭代的總次數。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
您應當確保每次迭代的代碼做合理數量的工作。和任何塊或函數調度到隊列一樣,調度代碼執行有開銷。如果每次循環迭代只執行很少量的工作,調度代碼的開銷可能超過調度到隊列帶來的性能優勢。如果在測試過程中發現這是真的,您可以使用跨步來增加每次循環迭代執行的工作量。隨著跨步,將原來循環的多次迭代組成一個單獨的塊,減少迭代次數的比例。例如,如果最初執行100次迭代,但決定使用步幅4,現在每個塊執行4次循環迭代,迭代次數是25。有關如何實現跨步,請參閱完善循環代碼
在主線程中執行任務
GCD提供特殊的串行調度隊列,可以使用它在應用程序的主線程上執行任務。這個隊列被自動的提供給所有應用程序,并由在主線程上設置的運行循環(被CFRunLoopRef
類型或者NSRunLoop
對象管理)drain(官方文檔上寫的drain,翻譯成銷毀感覺不太恰當,因為drain的意思是"使...流盡","耗盡"的意思,可以理解為使隊列中的任務全部出隊,NSAutoreleasePool也有drain方法,意思相近)。如果您創建的不是Cocoa應用程序,不要想著顯式的設置RunLoop,您必須顯式的調用dispatch_main
函數來drain主調度隊列。您仍然可以添加任務到隊列,但是如果您不調用此方法,這些任務永遠不會執行。
可以通過調用dispatch_get_main_queue
函數獲取應用程序主線程的調度隊列。添加到這個隊列的任務在主線程上被串行執行。因此,可以使用這個隊列作為應用程序其他部分工作執行完成的同步點。
在任務中使用Objective-C對象
GCD提供內置的支持Cocoa內存管理技術,所以,您可以自由的在提交到隊列的塊中使用Objective-C對象。每個調度隊列維護自己的自動釋放池來確保自動釋放對象在一些點被釋放;隊列不保證這些對象何時真正釋放。
如果您的應用程序內存不足,且您的塊創建超過幾個自動釋放對象,創建自己的自動釋放池是唯一的方法來確保您的對象被及時釋放。如果您的塊創建上百個對象,您可能希望創建多個自動釋放池或定期drain自動釋放池。
關于自動釋放池和Objective-C內存管理的更多信息,請參閱高級內存管理編程指南
暫停和恢復隊列
您可以通過暫停隊列來臨時阻止隊列執行塊對象。使用dispatch_suspend
函數來暫停調度隊列,使用dispatch_resume
函數來恢復調度隊列。調用dispatch_suspend
增加隊列的暫停引用計數,調用dispatch_resume
減少引用計數。當引用計數大于0時,隊列保持掛起。因此,為了恢復處理塊,您必須使用一個配對的恢復調用平衡所有暫停調用。
重要提示:暫停和恢復調用是異步的,僅在執行塊之間生效。暫停一個隊列不會導致已經執行的塊停止。
使用調度信號量來調節有限資源的使用
如果提交到調度隊列的任務訪問一些有限的資源,您可能需要使用調度信號來調節同時訪問資源的任務數量。調度信號像普通信號一樣工作,但有一個例外。當資源可用時,它獲取調度信號量消耗的時間比獲取傳統系統信號量消耗的時間少。這是因為GCD在這種特殊情況下不調用內核。只有當資源不可用且系統需要停駐線程直到向信號量發出信號時才調用內核。
使用信號量語義如下:
- 當創建信號量時(使用
dispatch_semaphore_create
函數),您可以指定一個正數,表示可用資源的數量。 - 在每個任務中,調用
dispatch_semaphore_wait
函數等待信號。 - 當等待返回時,獲取資源,執行工作。
- 當資源使用完畢時,釋放資源并調用
dispatch_semaphore_signal
函數向信號量發出信號。
有關這些步驟如何工作,例如,考慮在系統上使用文件描述符,每個應用程序被給予有限數量的文件描述符來使用。如果您有一個處理大量文件的任務,您不想一次打開這么多的文件,這樣會耗盡文件描述符。您可以在文件處理代碼中使用信號量限制任何時候文件描述符一次使用的數量。可能在您任務中添加的代碼基本片段如下:
// 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);
當創建一個信號量時,指定可用資源數量。這個值將成為信號量計數的初始值。每次等待信號,dispatch_semaphore_wait
函數將計數變量減1。如果結果值為負數,函數告訴內核阻塞線程。另外一邊,dispatch_semaphore_signal
函數將計數變量增加1,指示資源已經被釋放。如果有被阻塞且等待資源的任務,他們其中的一個隨后變為非阻塞并允許工作。
等待排隊任務組
調度組是阻塞線程直到一個或者多個任務結束執行的方法。您可以在不能夠獲取進度直到所有指定任務結束的地方使用這種行為。例如,調度幾個任務來計算一些數據,您可能使用一個組來等待這些任務,然后當它們結束時處理結果。使用調度組的另外一種方法是替代線程連接。您可能添加相應的任務到調度組且等待整個組,而不是開啟幾個子線程然后連接它們。
下面代碼顯示創建一個組,調度任務給它,并等待結果。使用dispatch_group_async
函數,而不是使用dispatch_async
函數調度任務到隊列。這個函數關聯任務到組,將它們排隊執行。為了等待任務組結束,稍后使用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_group_notify
或者dispatch_group_notify_f
函數來通知關聯到組的調度隊列執行完畢。也就是說當調度到隊列的塊都執行完畢的時候,會執行dispatch_group_notify
或者dispatch_group_notify_f
函數。
調度隊列和線程安全
在調度隊列的內容中討論線程安全可能看起來很奇怪,但線程安全仍然是相關聯的話題。任何時候在應用程序中實現并發,有幾件事情都應該知道:
- 調度隊列自身是線程安全的。換句話說,您可以從系統的任何線程提交任務到調度隊列,而不用先使用鎖或者同步訪問隊列。
- 不要從隊列中執行的任務里調用
dispatch_sync
函數 ,且傳遞給函數同一個隊列。這么做會導致隊列死鎖。如果您需要調度到當前隊列,異步使用dispatch_async
函數。 - 避免在提交給調度隊列的任務中使用鎖。雖然在任務中使用鎖是安全的,當您獲取鎖時,如果鎖不可用,可能阻塞整個串行隊列。相同的,對于并發隊列,等待鎖可能阻止其他線程執行。如果您需要同步部分代碼,使用串行調度隊列代替鎖。
- 雖然您可以獲取關于底層線程運行任務的信息,最好避免這么做。有關調度隊列和線程兼容性的更多信息,請參閱POSIX線程的兼容性
有關如何更改現有線程代碼到使用調度隊列的更多提示,請參閱線程遷移。
參考: