GCD簡介
GCD 是
libdispatch
的市場名稱,而 libdispatch 作為 Apple 的一個庫,為并發代碼在多核硬件(iOS 或 OS X )上執行提供有力支持。它具有以下優點:
- GCD 能通過推遲昂貴計算任務并在后臺運行它們來改善你的應用的響應性能。
- GCD 提供一個易于使用的并發模型而不僅僅只是鎖和線程,以幫助我們避開并發陷阱。
- GCD 具有在常見模式(例如單例)上用更高性能的原語優化你的代碼的潛在能力。
為了讓開發者更加容易的使用設備上的多核CPU,蘋果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD)。Grand Central Dispatch (GCD) 是 Apple 開發的多線程編程解決方法。
通過 GCD,開發者不用再直接跟線程打交道了,只需要向隊列中添加代碼塊即可,GCD 在后端管理著一個線程池。GCD 不僅決定著你的代碼塊將在哪個線程被執行,它還根據可用的系統資源對這些線程進行管理。這樣可以將開發者從線程管理的工作中解放出來,通過集中的管理線程,來緩解大量線程被創建的問題。
GCD 帶來的另一個重要改變是,作為開發者可以將工作考慮為一個隊列,而不是一堆線程,這種并行的抽象模型更容易掌握和使用。
GCD 公開有 5 個不同的隊列:運行在主線程中的 main queue,3 個不同優先級的后臺隊列,以及一個優先級更低的后臺隊列(用于 I/O)。 另外,開發者可以創建自定義隊列:串行或者并行隊列。自定義隊列非常強大,在自定義隊列中被調度的所有 block 最終都將被放入到系統的全局隊列中和線程池中。
GCD 與 操作隊列 (operation queue)
GCD 是純 C 的 API,而操作隊列則是 Objective-C 的對象。
操作隊列在底層是用 GCD 來實現的。
進程、線程、隊列、串行、并發、同步、異步、傻傻分不清楚
Thread 線程
線程(thread)是組成進程的子單元,操作系統的調度器可以對線程進行單獨的調度。實際上,所有的并發編程 API 都是構建于線程之上的 —— 包括 GCD 和操作隊列(operation queues)。
多線程可以在單核 CPU 上同時(或者至少看作同時)運行。操作系統將小的時間片分配給每一個線程,這樣就能夠讓用戶感覺到有多個任務在同時進行。如果 CPU 是多核的,那么線程就可以真正的以并發方式被執行,從而減少了完成某項操作所需要的總時間。
Synchronous vs. Asynchronous 同步 vs. 異步
描述一個函數/方法與一個任務之間的關系,該函數要求一個任務在GCD下執行。
同步函數:方法會在指定的任務完成之后才返回。
異步函數:方法會立即返回,預定的任務會完成但不會等待它完成。因此,一個異步函數不會阻塞當前線程去執行下一個函數。
區別:方法是否立即返回。
Critical Section 臨界區
就是一段代碼不能被并發執行,也就是,兩個線程不能同時執行這段代碼。這很常見,因為代碼去操作一個共享資源,例如一個變量若能被并發進程訪問,那么它很可能會變質(譯者注:它的值可能被動態更改了但是還沒有返回)
Race Condition 競態條件
這種狀況是指基于特定序列或時機的事件的軟件系統以不受控制的方式運行的行為,例如程序的并發任務執行的確切順序。競態條件可導致無法預測的行為,而不能通過代碼檢查立即發現。
關于競態條件更好的詮釋可以參考 objc.io 的例子:
Deadlock 死鎖
兩個(或更多)東西——在大多數情況下,是線程——所謂的死鎖是指它們都卡住了,并等待對方完成或執行其它操作。第一個不能完成是因為它在等待第二個的完成。但第二個也不能完成,因為它在等待第一個的完成。【與內存管理的循環引用問題類似】
互斥鎖
互斥訪問的意思就是同一時刻,只允許一個線程訪問某個特定資源。為了保證這一點,每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖,一旦某個線程對資源完成了操作,就釋放掉這個互斥鎖,這樣別的線程就有機會訪問該共享資源了。
優先級反轉
優先級反轉是指程序在運行時低優先級的任務阻塞了高優先級的任務,有效的反轉了任務的優先級。由于 GCD 提供了擁有不同優先級的后臺隊列,甚至包括一個 I/O 隊列,所以我們最好了解一下優先級反轉的可能性。
高優先級和低優先級的任務之間共享資源時,就可能發生優先級反轉。當低優先級的任務獲得了共享資源的鎖時,該任務應該迅速完成,并釋放掉鎖,這樣高優先級的任務就可以在沒有明顯延時的情況下繼續執行。然而高優先級任務會在低優先級的任務持有鎖的期間被阻塞。如果這時候有一個中優先級的任務(該任務不需要那個共享資源),那么它就有可能會搶占低優先級任務而被執行,因為此時高優先級任務是被阻塞的,所以中優先級任務是目前所有可運行任務中優先級最高的。此時,中優先級任務就會阻塞著低優先級任務,導致低優先級任務不能釋放掉鎖,這也就會引起高優先級任務一直在等待鎖的釋放。
解決這個問題的方法,通常就是不要使用不同的優先級。通常最后你都會以讓高優先級的代碼等待低優先級的代碼來解決問題。當你使用 GCD 時,總是使用默認的優先級隊列(直接使用,或者作為目標隊列)。如果你使用不同的優先級,很可能實際情況會讓事情變得更糟糕。
從中得到的教訓是,使用不同優先級的多個隊列聽起來雖然不錯,但畢竟是紙上談兵。它將讓本來就復雜的并行編程變得更加復雜和不可預見。如果你在編程中,遇到高優先級的任務突然沒理由地卡住了,可能你會想起本文,以及那個美國宇航局的工程師也遇到過的被稱為優先級反轉的問題。
Thread Safe 線程安全
線程安全的代碼能在多線程或并發任務中被安全地調用,而不會導致任何問題(數據損壞,崩潰等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary
。你可以在同一時間在多個線程中使用它而不會有問題。相反,NSMutableDictionary
就不是線程安全的,應該保證一次只能有一個線程訪問它。
Context Switch 上下文切換
一個上下文切換:在單個進程里切換執行不同的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很普遍,但會帶來一些額外的開銷。
Concurrency vs Parallelism 并發與并行
并發代碼的不同部分可以“同步”執行。然而,該怎樣發生或是否發生都取決于系統。多核設備通過并行來同時執行多個線程;然而,為了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,然后運行另一個線程或進程。這通常發生地足夠快以致給我們并發執行地錯覺,如下圖所示:
雖然你可以編寫代碼在 GCD 下并發執行,但 GCD 會決定有多少并行的需求。并行要求并發,但并發不能保證并行。
Queues 隊列
GCD 提供 dispatch queues
來處理代碼塊,這些隊列管理你提供給 GCD 的任務并用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列里的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列結束。
所有的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程并行的訪問它們。當你了解了調度隊列如何為你自己代碼的不同部分提供線程安全后,GCD的優點就會顯而易見。關于這一點的關鍵是:選擇正確類型的調度隊列和正確的調度函數來提交你的工作。
Serial vs. Concurrent 串行 vs. 并發
描述當前任務與其它被執行任務之間的關系
串行隊列:每次只有一個任務被執行;
并發隊列:同一時間可以有多個任務被執行。
Serial Queues 串行隊列
串行隊列中的任務一次執行一個,每個任務只有在前一個任務完成后才會開始。而且,你不能確定 Block 任務執行的時間長度,如下圖所示:
這些任務的執行時機受到 GCD 的控制;唯一能確保的事情是: GCD 一次只執行一個任務,并且按照我們添加到隊列中的順序來執行。
由于在串行隊列中不會有兩個任務并發執行,因此不會出現同時訪問臨界區的風險;相對于這些任務來說,這就從競態條件下保護了臨界區。所以如果訪問臨界區的唯一方式是通過提交到調度隊列的任務,那么你就不需要擔心臨界區的安全問題了。
Concurrent Queues 并發隊列
并發隊列中的任務能得到的保證是:它們會按照被添加的順序開始執行,但這就是全部的保證了。任務可能以任意順序完成,你不知道何時開始執行下一個任務,或者何時有多少 Block 任務在執行。再說一遍,這完全取決于 GCD 。
下圖展示了一個示例任務執行計劃,GCD 管理著四個并發任務:
注意 Block 1、2 和 3 都是立即執行的,一個接一個。在 Block 0 開始后,Block 1等待了好一會兒才開始。同樣, Block 3 在 Block 2 之后才開始,但它先于 Block 2 完成。
何時開始一個 Block 完全取決于 GCD 。如果一個 Block 的執行時間與另一個重疊,也是由 GCD 來決定是否將其運行在另一個不同的核心上,如果那個核心可用,就用上下文切換的方式來執行不同的 Block 。
有趣的是, GCD 給你提供了至少五個特定的隊列,可根據隊列類型選擇使用。
Queue Types 隊列類型
首先,系統給你提供了一個叫做 主隊列(main queue)
的特殊隊列。和其它串行隊列一樣,該隊列中的任務一次只能執行一個。然而,它能保證所有的任務都在主線程執行,而主線程是唯一可用于更新 UI 的線程。這個隊列用于發送消息或通知給 UIView
、響應用戶交互。
同時,系統給你提供了幾個并發隊列。它們叫做 全局調度隊列(Global Dispatch Queues)
。目前的四個全局隊列有著不同的優先級:background
、low
、default
以及 high
。要知道,Apple 的 API 也會使用這些隊列,所以你添加的任何任務都不會是這些隊列中唯一的任務。
最后,你也可以創建自己的串行隊列或并發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你自己創建的隊列。
以上是調度隊列的大框架!
GCD 的“藝術”歸結為選擇合適隊列的調度函數以提交你的工作。
GCD 方法
GCD 是純 C 的 API,如果你使用 MBProgressHUD 框架,你會看到使用 GCD 的這個示例:
// 在根視圖上顯示 HUD
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
// 異步后臺線程執行,使UIKit有機會重新繪制 HUD 并添加到視圖層次結構中。
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// Do something useful in the background
[self doSomeWork];
// 主線程執行,請確保始終在主線程上更新UI(包括MBProgressHUD)。
dispatch_async(dispatch_get_main_queue(), ^{
[hud hideAnimated:YES];
});
});
全局隊列, dispatch_get_global_queue()
函數:dispatch_get_global_queue(long identifier, unsigned long flags)
-
參數一:
long identifier
:qos_class_t 中定義的服務質量或者 dispatch_queue_priority_t 中定義的隊列優先級。-
dispatch_queue_priority_t :隊列優先級
// 派發到隊列中的任務將以最高優先級運行,此隊列任務將會被安排到默認優先級或者低優先級的任務之前運行。 #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 派發到隊列中的任務將以默認優先級運行,即,該任務將會在【所有高優先級任務已經被調度之后,并且在所有低優先級任務被調度之前】被調度執行。 #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 派發到隊列中的任務將以低優先級運行,即在所有默認優先級和高優先級任務已經被執行之后,該隊列中的任務將會被調度執行 #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 派發到隊列中的任務將以后臺優先級運行,也就是說,在調度了所有高優先級任務之后,該隊列中的任務將被調度執行,并且系統將在具有根據 setpriority 的后臺狀態的線程上運行該隊列上的任務。(磁盤 I/O 受到限制,線程的調度優先級設置為最低值)。 #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
-
qos_class_t:服務質量,iOS 8.0 新增
* Apple 建議我們使用服務質量類別的值來標記全局并發隊列 * - QOS_CLASS_USER_INTERACTIVE * - QOS_CLASS_USER_INITIATED * - QOS_CLASS_DEFAULT * - QOS_CLASS_UTILITY * - QOS_CLASS_BACKGROUND * * 全局并發隊列仍然可以通過優先級來識別,它會被映射到以下QOS類: * - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED * - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT * - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY * - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND /*! * @constant QOS_CLASS_USER_INTERACTIVE * @abstract 該線程執行的是與用戶交互的工作 * @discussion 與系統上運行的其他工作相比,該工作被要求以最高優先級運行。指定這個 QOS 類會請求幾乎所有可用的系統 CPU 和 I/O 帶寬運行,甚至不惜爭用資源。 * 這不是一個適用于大型任務的節能 QOS 類。 這個 QOS 類的使用應限于與用戶的關鍵交互,例如處理主事件循環中的事件,繪圖,動畫等。 * * @constant QOS_CLASS_USER_INITIATED * @abstract 該線程執行的工作的 QOS 類是由用戶發起的并且用戶可能正在等待結果。 * @discussion 這種工作的優先級低于關鍵的用戶交互工作,但比系統上的其他工作要高。 * 這不是一個適用于大型任務的節能 QOS 類。它的使用應該限制在足夠短的時間內,以至于用戶不太可能在等待結果的時候切換任務。 典型的用戶發起的通過顯示占位符內容或模態用戶界面來指示進度的工作。 * * @constant QOS_CLASS_DEFAULT * @abstract 系統在缺少更具體的QOS分類信息的情況下使用的默認QOS分類。 * @discussion 這種工作優先級低于關鍵的用戶交互和用戶發起的工作,但比實用工具和后臺任務要高。 由pthread_create()創建的沒有指定QOS類的屬性的線程將默認為QOS_CLASS_DEFAULT。 這個QOS類的值并不打算作為工作分類,只有在傳播或恢復系統提供的QOS類值時才應設置。 * * @constant QOS_CLASS_UTILITY * @abstract 該線程執行的工作的QOS類或許可能由用戶發起,并且用戶并不期待立即等待結果。 * @discussion 這種工作的優先級低于關鍵用戶交互和用戶啟動的工作,但比低級系統維護任務要高。 使用這個QOS級別表明工作應該以節能和高效率的方式運行。 效用工作的進展可能會也可能不會被指示給用戶,但這種工作的效果是用戶可見的。 * * @constant QOS_CLASS_BACKGROUND * @abstract 該線程執行的工作的QOS類不是由用戶發起的,并且用戶可能不知道結果。 * @discussion 這項工作的優先級低于其他工作。 使用這個QOS級別表明工作應該以最節能,最高效的方式進行。 * * @constant QOS_CLASS_UNSPECIFIED * @abstract 指示QOS類信息的缺失或移除的QOS類值。 * @discussion 作為API返回值,可能表示線程或pthread屬性是使用與舊版API不兼容或與QOS類系統沖突的配置。 */ __QOS_ENUM(qos_class, unsigned int, QOS_CLASS_USER_INTERACTIVE __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x21, QOS_CLASS_USER_INITIATED __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x19, QOS_CLASS_DEFAULT __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x15, QOS_CLASS_UTILITY __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x11, QOS_CLASS_BACKGROUND __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x09, QOS_CLASS_UNSPECIFIED __QOS_CLASS_AVAILABLE(macos(10.10), ios(8.0)) = 0x00, ); #undef __QOS_ENUM
?
-
-
參數二:
unsigned long flags
Apple 保留以供將來使用的值。 傳遞除零以外的任何值都可能導致返回值為NULL。因此,這個值我們務必設置為0。
使用示例:
// 異步后臺隊列
dispatch_queue_t global_queue = dispatch_get_global_queue(0, 0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 異步后臺任務...
});
異步隊列, dispatch_async
三種方式:
異步自定義串行隊列
dispatch_queue_t my_queue = dispatch_queue_create("com.companyName.projectName.taskName", NULL); // 傳參 NULL 代表這是一個串行隊列。
dispatch_async(my_queue, ^{
// 異步自定義任務
});
當你想串行執行后臺任務并追蹤它時就是一個好選擇。這消除了資源爭用,因為你知道一次只有一個任務在執行。注意若你需要獲取某個方法的數據,你必須內聯另一個 Block 來找回它或考慮使用 dispatch_sync
。
異步主隊列
dispatch_async(dispatch_get_main_queue(), ^{
// 異步主線程任務
});
這是在一個并發隊列上完成任務后更新 UI 的共同選擇。要這樣做,你將在一個 Block 內部編寫另一個 Block 。以及,如果你在主隊列調用 dispatch_async
到主隊列,你能確保這個新任務將在當前方法完成后的某個時間執行。
異步全局隊列
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 異步并發任務
});
這是在后臺執行非 UI 工作的共同選擇。
延后執行任務,dispatch_after
延遲 n 秒執行,使用 snippet 代碼塊:==dispatch_after==
double delayInSeconds = 2.0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// code to be executed after a specified delay
});
dispatch_after
可以使任務延后執行,缺點是無法取消 block 中將要執行的代碼。如果你需要一些事情在某個特定的時刻運行,那么 dispatch_after
或許會是個好的選擇。確保同時考慮了 NSTimer
,這個API雖然有點笨重,但是它允許你取消定時器的觸發。
不知道何時適合使用 dispatch_after
?
- 自定義串行隊列:在一個自定義串行隊列上使用
dispatch_after
要小心。你最好堅持使用主隊列。 - ?主隊列(串行):是使用
dispatch_after
的好選擇;Xcode 提供了一個不錯的 snippet 模版(==dispatch_after==)。 - 并發隊列:在并發隊列上使用
dispatch_after
也要小心;你會這樣做就比較罕見。還是在主隊列做這些操作吧。
因此,強烈建議在主隊列上執行延后任務。
單次執行,dispatch_once
dispatch_once()
以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once
的代碼)的不同的線程會在臨界區已有一個線程的情況下被阻塞,直到臨界區完成為止。
使用 snippet 代碼塊創建:==dispatch_once==
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
該方法常用于單例類初始化,可以防止競態條件發生:
?不推薦的單例創建方法:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
原因在于,if 條件分支不是線程安全的。如果調用這個單例方法多次,系統可能對創建多個實例對象——競態條件。
?推薦的方法:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
柵欄,dispatch_barriers
Note:這些稱謂指的都是 dispatch_barriers 函數:柵欄、障礙、屏障...
讀寫問題
照片管理類可以對照片執行寫方法:
- (void)addPhoto:(Photo *)photo {
if (photo) {
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
}
}
也可以執行讀方法:
- (NSArray *)photos {
return _photosArray;
}
??問題在于,這兩個方法不能提供任何保護措施來對抗當一個線程調用讀方法 photos
的同時另一個線程調用寫方法 addPhoto:
。
GCD 通過用 dispatch barriers
創建一個讀寫鎖
提供了一個優雅的解決方案。
Dispatch barriers 是一組函數,在并發隊列上工作時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上唯一被執行的條目。這就意味著所有的先于調度障礙提交到隊列的條目必能在這個 Block 執行前完成。
當這個 Block 的時機到達,調度障礙執行這個 Block 并確保在那個時間里隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。
下圖顯示了障礙函數對多個異步隊列的影響:
注意到正常部分的操作就如同一個正常的并發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是唯一在執行的事物。在障礙完成后,隊列回到一個正常并發隊列的樣子。
下面是你何時會,或者不會使用障礙函數的情況:
- ?自定義串行隊列:一個很壞的選擇;障礙不會有任何幫助,因為不管怎樣,一個串行隊列一次只執行一個操作。
- ?全局并發隊列:要小心;這可能不是最好的主意,因為其它系統可能在使用隊列而且你不能壟斷它們只為你自己的目的。
- ?自定義并發隊列:這對于原子或臨界區代碼來說是極佳的選擇。任何你在設置或實例化的需要線程安全的事物都是使用障礙的最佳候選。
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
//...
/*
1. “寫操作”可以放入并發隊列,因為設置方法并不需要返回值;
2. 使用 dispatch_barrier 將此并發隊列加鎖,保證任一時刻只有一個“寫操作”在執行;
2. 自定義并發隊列,而不是全局并發隊列的原因是:全局隊列中還可能有其他任務,一旦加鎖就會阻塞其他任務的正常執行,不能為了個人利益影響國家嘛,因此我們開辟一個新的自定義并發隊列專門處理這個問題。
*/
- (void)addPhoto:(Photo *)photo {
if (photo) {
dispatch_barrier_async(self.concurrentPhotoQueue, ^{
[_photosArray addObject:photo];
dispatch_async(dispatch_get_main_queue(), ^{
[self postContentAddedNotification];
});
});
}
}
?? 注意:當使用并發隊列時,要確保所有的 barrier 調用都是 async 的。如果你使用 dispatch_barrier_sync
,那么你很可能會使你自己(更確切的說是,你的代碼)產生死鎖。寫操作需要 barrier,并且可以是 async 的。
自定義線程,dispatch_queue_create()
函數:dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t _Nullable attr)
參數一:const char * _Nullable label
,自定義線程的標識符,一般為反向DNS域名
參數二:指定隊列類型(串行/并發隊列),可以傳入的參數類型如下:
- 串行:
DISPATCH_QUEUE_SERIAL
:一個按FIFO順序調用塊的調度隊列。 - 并發:
DISPATCH_QUEUE_CONCURRENT
:使用DISPATCH_QUEUE_CONCURRENT
屬性創建的隊列可以同時調用塊(類似于全局并發隊列,但可能會有更多的開銷),并且,它支持使用調度柵欄 API (dispatch barriers
)提交的柵欄塊,例如, 能夠實現高效的讀寫器方案。
注意:當你在網上搜索例子時,你會經常看人們傳遞
0
或者NULL
給dispatch_queue_create
的第二個參數。這是創建串行隊列的過時方式;明確你的參數總是更好。
使用 dispatch_queue_create
方法
dispatch_queue_t urls_queue = dispatch_queue_create("com.companyName.project.taskName", NULL);
dispatch_async(urls_queue, ^{
// your code
});
修改使用自定義隊列:
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
//...
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
// 自定義并發隊列
sharedPhotoManager -> _concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT);
});
return sharedPhotoManager;
}
同步隊列,dispathc_sync
dispatch_sync()
同步地提交工作并在返回前等待它完成。使用 dispatch_sync
跟蹤你的調度障礙工作,或者當你需要等待操作完成后才能使用 Block 處理過的數據。如果你使用第二種情況做事,你將不時看到一個 __block
變量寫在 dispatch_sync
范圍之外,以便返回時在 dispatch_sync
使用處理過的對象。
但你需要很小心。想像如果你調用 dispatch_sync
并放在你已運行著的當前隊列。這會導致死鎖,因為調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務無法完成!這將迫使你自覺于你正從哪個隊列調用——以及你正在傳遞進入哪個隊列。
下面是一個快速總覽,關于在何時以及何處使用 dispatch_sync
:
- 自定義串行隊列:在這個狀況下要非常小心!如果你正運行在一個隊列并調用
dispatch_sync
放在同一個隊列,那你就百分百地創建了一個死鎖(同步隊列嵌套會導致死鎖問題!!!) - 主隊列(串行):同上面的理由一樣,必須非常小心!這個狀況同樣有潛在的導致死鎖的情況。
- ?并發隊列:這才是做同步工作的好選擇,不論是通過調度障礙,或者需要等待一個任務完成才能執行進一步處理的情況。
// 將“讀操作”放入同步隊列,這樣方法就不會立即返回了,它會等待執行“讀操作”完畢后才返回。
- (NSArray *)photos
{
// __block 關鍵字允許對象在 Block 內可變。沒有它,array 在 Block 內部就只是只讀的,你的代碼甚至不能通過編譯。
__block NSArray *array;
dispatch_sync(self.concurrentPhotoQueue, ^{
array = [NSArray arrayWithArray:_photosArray];
});
return _photosArray;
}
小結:讀寫的線程安全問題
一、以上幾個函數是如何解決讀寫問題的呢?
-
寫方法:將寫操作放在
dispatch_barrier_async
阻塞函數中,同時,該阻塞函數運行在自定義并發隊列中:dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)
。 -
讀方法:將讀操作放入同步隊列
dispatch_sync
中,方法會等待讀操作執行完畢后再返回,同時,該同步隊列仍然運行在自定義并發隊列中:因為多個獲取方法可以并發執行。 - 獲取方法與設置方法之間不能并發執行,因此我們將他們放在同一個自定義并發隊列中。
dispatch_sync 與 dispatch_async 的區別
-
dispatch_sync
,同步隊列,函數會等待它返回后再繼續執行。 -
dispatch_async
,異步隊列,函數會立即返回。
dispatch_sync
viewDidLoad
方法會等待同步隊列任務返回后再繼續執行。
dispatch_async
viewDidLoad
方法會立即繼續執行,不會等待異步隊列任務返回。
調度組,dispatch_group_t
Dispatch Group 會在整個組的任務都完成時通知你。這些任務可以是同步的,也可以是異步的,即便在不同的隊列也行。而且在整個組的任務都完成時,Dispatch Group 可以用同步的或者異步的方式通知你。因為要監控的任務在不同隊列,那就用一個 dispatch_group_t
的實例來記下這些不同的任務。
當組中所有的事件都完成時,GCD 的 API 提供了兩種通知方式。
示例代碼一:
// 讓后臺 2 個線程并行執行,然后等 2 個線程都結束后,再匯總執行結果。
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 并行執行線程一
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 并行執行線程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
// 匯總結果
});
因為下載任務是在后臺執行的,它會立即返回,如何監聽下載完成并做一些下載完成后的處理?使用 dispatch_group_t
。
第一種是 dispatch_group_wait
,它會阻塞當前線程,直到組里面所有的任務都完成或者等到某個超時發生。
1?? dispatch_group_wait() 函數,會阻塞主線程:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
// 因為 dispatch_group_wait() 方法會阻塞主線程,所以使用 dispatch_async() 將整個方法放入后臺隊列中以避免阻塞主線程。
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ // 1
__block NSError *error;
// 創建一個任務調度組
dispatch_group_t downloadGroup = dispatch_group_create(); // 2
for (NSInteger i = 0; i < 3; i++) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
// dispatch_group_enter 手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對出現,否則你可能會遇到詭異的崩潰問題。
dispatch_group_enter(downloadGroup); // 3
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 4
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
// dispatch_group_wait 會一直等待,直到任務全部完成或者超時。如果在所有任務完成前超時了,該函數會返回一個非零值。你可以對此返回值做條件判斷以確定是否超出等待周期;然而,你在這里用 DISPATCH_TIME_FOREVER 讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,因為圖片的創建工作總是會完成的。
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
// 此時此刻,你已經確保了,要么所有的圖片任務都已完成,要么發生了超時。然后,你在主線程上運行 completionBlock 回調。這會將工作放到主線程上,并在稍后執行。
dispatch_async(dispatch_get_main_queue(), ^{ // 6
if (completionBlock) {
completionBlock(error);
}
});
});
}
2?? dispatch_group_notify() 函數,異步執行,不會阻塞主線程:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error;
// 創建一個任務調度組
dispatch_group_t downloadGroup = dispatch_group_create(); // 1
for (NSInteger i = 0; i < 3; i++) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup); // 2
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 3
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
// dispatch_group_notify 以異步的方式工作。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那么 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所需要的。
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
if (completionBlock) {
completionBlock(error);
}
});
}
關于何時以及怎樣使用有著不同的隊列類型的 Dispatch Group :
- 自定義串行隊列:它很適合當一組任務完成時發出通知。
- 主隊列(串行):它也很適合這樣的情況。但如果你要同步地等待所有工作地完成,那你就不應該使用它,因為你不能阻塞主線程。然而,異步模型是一個很有吸引力的能用于在幾個較長任務(例如網絡調用)完成后更新 UI 的方式。
- 并發隊列:它也很適合 Dispatch Group 和完成時通知。
并發循環、迭代執行,dispatch_apply
dispatch_apply
表現得就像一個 for
循環,但它能并發地執行不同的迭代。這個函數是同步的,所以和普通的 for
循環一樣,它只會在所有工作都完成后才會返回。
當在 Block 內計算任何給定數量的工作的最佳迭代數量時,必須要小心,因為過多的迭代和每個迭代只有少量的工作會導致大量開銷以致它能抵消任何因并發帶來的收益。而被稱為跨越式(striding)
的技術可以在此幫到你,即通過在每個迭代里多做幾個不同的工作。
譯者注:大概就能減少并發數量吧,作者是提醒大家注意并發的開銷,記在心里!
那何時才適合用 dispatch_apply
呢?
- 自定義串行隊列:串行隊列會完全抵消
dispatch_apply
的功能;你還不如直接使用普通的for
循環。 - 主隊列(串行):與上面一樣,在串行隊列上不適合使用
dispatch_apply
。還是用普通的for
循環吧。 - ?并發隊列:對于并發循環來說是很好選擇,特別是當你需要追蹤任務的進度時。
使用示例:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error;
// 創建一個任務調度組
dispatch_group_t downloadGroup = dispatch_group_create(); // 1
// 使用 dispatch_apply 代替 for (NSInteger i = 0; i < 3; i++) 循環
dispatch_apply(3, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^(size_t i) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup); // 2
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 3
}];
[[PhotoManager sharedManager] addPhoto:photo];
});
// dispatch_group_notify 以異步的方式工作。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那么 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所需要的。
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
if (completionBlock) {
completionBlock(error);
}
});
}
實際上,在這個例子里并不值得。下面是原因:
- 你創建并行運行線程而付出的開銷,很可能比直接使用
for
循環要多。若你要以合適的步長迭代非常大的集合,那才應該考慮使用dispatch_apply
。 - 你用于創建應用的時間是有限的——除非實在太糟糕否則不要浪費時間去提前優化代碼。如果你要優化什么,那去優化那些明顯值得你付出時間的部分。你可以通過在 Instruments 里分析你的應用,找出最長運行時間的方法。看看 如何在 Xcode 中使用 Instruments 可以學到更多相關知識。
- 通常情況下,優化代碼會讓你的代碼更加復雜,不利于你自己和其他開發者閱讀。請確保添加的復雜性能換來足夠多的好處。
記住,不要在優化上太瘋狂。你只會讓你自己和后來者更難以讀懂你的代碼。
??使用線程并不是沒有代價的,線程會有創建時的時間開銷,還會消耗內核的內存,即應用的內存空間。
信號量函數,dispatch_semaphore_t
信號量讓你控制多個消費者對有限數量資源的訪問。舉例來說,如果你創建了一個有著兩個資源的信號量,那同時最多只能有兩個線程可以訪問臨界區。其他想使用資源的線程必須在一個…你猜到了嗎?…FIFO隊列里等待。
...
每秒執行,dispatch_source
幫助你去響應或監測 Unix 信號、文件描述符、Mach 端口、VFS 節點,以及其它晦澀的東西。
常用于倒計時器,使用 snippet 代碼塊:==dispatch_source==
示例一:
// 60秒倒計時定時器,每秒執行一次,
double intervalInSeconds = 60.0; // 時間間隔
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, intervalInSeconds * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
// code to be executed when timer fires
});
dispatch_resume(timer);
示例二:
// 點擊“獲取驗證碼按鈕”,開啟60秒倒計時
-(void)buttonCountDown:(UIButton *)button {
button.enabled = NO;
__block int timeout = 60; // 倒計時時間
NSTimeInterval intervalInSeconds = 1.0; // 執行時間間隔
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, intervalInSeconds * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
if (timeout <= 0) {
dispatch_source_cancel(timer);
dispatch_async(dispatch_get_main_queue(), ^{
button.enabled = YES;
[button setTitle:@"重新獲取驗證碼" forState:UIControlStateNormal];
});
}else {
dispatch_async(dispatch_get_main_queue(), ^{
button.enabled = NO;
NSString *buttonTitle = [NSString stringWithFormat:@"%ds后重新發送",timeout];
[button setTitle:buttonTitle forState:UIControlStateNormal];
});
}
timeout --;
});
dispatch_resume(timer);
}
參考文獻
-
簡單描述了 block、GCD 的幾個方法。
-
Ray Wenderlich: iOS系統中的多線程和GCD的初學者教程
Ray Wenderlich 系列的教程,非常值得一看,只不過這篇教程是2011年寫的,年代有些久遠,教程中還使用了 ASIHTTPRequest(一個已經停止更新的網絡庫,目前流行的是 AFNetworking),參考起來有些費解,教程的大致內容總結如下:
教程講解了一個 ImageGrabber 應用,它的功能是打開 HTML 網頁,檢索并抓取所有圖片的鏈接,然后將所有抓取到的圖片顯示在列表視圖上。它還可以下載 zip 文件,并提取出 zip 文件內的圖像。
但是,如果應用程序在主線程上執行所有的操作:解析HTML、下載圖片、下載并解壓 zip 文件、遍歷并提取出 zip 文件中所有圖片。這樣會導致主線程阻塞造成UI界面卡頓或者無法響應用戶交互,用戶不得不等待大量的時間,也不知道應用程序是否仍然正常工作!
優化一:異步下載,并使用 NSNotification 通知更新UI。
優化二:使用GCD將下載并解壓 zip 文件的耗時任務放到后臺線程執行。
不建議:在主線程上執行所有的任務。
推薦:把一個進程分解成多個線程執行。主線程用于更新UI,響應用戶事件,后臺線程用于檢索HTML頁面、下載并解壓縮文件。
-
??????
-
??????
Ray Wenderlich: NSOperation and NSOperationQueue Tutorial in Swift
-
全局隊列函數
dispatch_get_global_queue()
及其參數。 -
并發編程面臨的挑戰:資源共享導致的競態條件、互斥鎖、死鎖、資源饑餓、優先級反轉。
?
我們建議采納的安全模式是這樣的:從主線程中提取出要使用到的數據,并利用一個操作隊列在后臺處理相關的數據,最后回到主隊列中來發送你在后臺隊列中得到的結果。使用這種方式,你不需要自己做任何鎖操作,這也就大大減少了犯錯誤的幾率。
Apple沒有把 UIKit 設計為線程安全的類是有意為之的,將其打造為線程安全的話會使很多操作變慢。而事實上 UIKit 是和主線程綁定的,這一特點使得編寫并發程序以及使用 UIKit 十分容易的,你唯一需要確保的就是對于 UIKit 的調用總是在主線程中來進行。
-
Apple: Threading Programming Guide
蘋果官方文檔,描述了OS X 和 iOS 中一些關鍵框架的高級線程安全性。
-
Matt Galloway. Effective Objective-C 2.0: 編寫高質量 iOS 與 OS X 代碼的52個有效方法[M] .北京: 機械工業出版社,2014: 149-180.
- 第41條:多用派發隊列,少用同步鎖。
這一條是針對讀寫的競態條件而言的。作者推薦使用 GCD 方法中的同步隊列以及柵欄塊實現同步語義,而不是 @synchronized 塊或者 NSLock 對象。
- 第42條:多用GCD,少用 performSelector 系列方法。
performSelector 系列方法本身有許多不足:內存管理方面有疏忽、可處理的方法也有局限。
performSelector 系列方法中有延后執行選擇子,或者將其放在另一個線程上執行的方法,這些我們都可以通過 GCD 提供的方法來替換實現。
- 第43條:掌握 GCD 及操作隊列的使用時機。
在解決多線程與任務管理問題時,派發隊列并非唯一方案。
操作隊列提供了一套高層的 Objective-C API,能實現純 GCD 所具備的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
- 第44條:通過 Dispatch Group 機制,根據系統資源狀況來執行任務。
- 第45條:使用 dispatch_once 來執行只需運行一次的線程安全代碼。
單例類的創建問題。
- 第46條:不要使用 dispatch_get_current_queue。
dispatch_get_current_queue 函數的行為常常與開發者所預期的不同。此函數已廢棄,只應做調試之用。
由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述當前隊列這一概念。
dispatch_get_current_queue 函數用于解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決。
Gaurav Vaish. 高性能 iOS 應用開發[M] .北京: 人民郵電出版社,2017: 89-116.