GCD介紹(二):多核心的性能
概念
為了在單一進程中充分發揮多核的優勢,我們有必要使用多線程技術(我們沒必要去提多進程,這玩意兒和GCD沒關系)。在低層,GCD全局dispatch queue僅僅是工作線程池的抽象。這些隊列中的Block一旦可用,就會被dispatch到工作線程中。提交至用戶隊列的Block最終也會通過全局隊列進入相同的工作線程池(除非你的用戶隊列的目標是主線程,但是為了提高運行速度,我們絕不會這么干)。
有兩種途徑來通過GCD“榨取”多核心系統的性能:將單一任務或者一組相關任務并發至全局隊列中運算;將多個不相關的任務或者關聯不緊密的任務并發至用戶隊列中運算;
全局隊列
設想下面的循環:
- for(id obj in array)
- [self doSomethingIntensiveWith:obj];
假定 -doSomethingIntensiveWith: 是線程安全的且可以同時執行多個.一個array通常包含多個元素,這樣的話,我們可以很簡單地使用GCD來平行運算:
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- for(id obj in array)
- dispatch_async(queue, ^{
- [self doSomethingIntensiveWith:obj];
- });
如此簡單,我們已經在多核心上運行這段代碼了。
當然這段代碼并不完美。有時候我們有一段代碼要像這樣操作一個數組,但是在操作完成后,我們還需要對操作結果進行其他操作:
- for(id obj in array)
- [self doSomethingIntensiveWith:obj];
- [self doSomethingWith:array];
這時候使用GCD的 dispatch_async 就悲劇了.我們還不能簡單地使用dispatch_sync來解決這個問題, 因為這將導致每個迭代器阻塞,就完全破壞了平行計算。
解決這個問題的一種方法是使用dispatch group。一個dispatch group可以用來將多個block組成一組以監測這些Block全部完成或者等待全部完成時發出的消息。使用函數dispatch_group_create來創建,然后使用函數dispatch_group_async來將block提交至一個dispatch queue,同時將它們添加至一個組。所以我們現在可以重新編碼:
- dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_group_t group = dispatch_group_create();
- for(id obj in array)
- dispatch_group_async(group, queue, ^{
- [self doSomethingIntensiveWith:obj];
- });
- dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
- dispatch_release(group);
- [self doSomethingWith:array];
如果這些工作可以異步執行,那么我們可以更風騷一點,將函數-doSomethingWith:放在后臺執行。我們使用dispatch_group_async函數建立一個block在組完成后執行:
- dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_group_t group = dispatch_group_create();
- for(id obj in array)
- dispatch_group_async(group, queue, ^{
- [self doSomethingIntensiveWith:obj];
- });
- dispatch_group_notify(group, queue, ^{
- [self doSomethingWith:array];
- });
- dispatch_release(group);
不僅所有數組元素都會被平行操作,后續的操作也會異步執行,并且這些異步運算都會將程序的其他部分的負載考慮在內。注意如果-doSomethingWith:需要在主線程中執行,比如操作GUI,那么我們只要將main queue而非全局隊列傳給dispatch_group_notify函數就行了。
對于同步執行,GCD提供了一個簡化方法叫做dispatch_apply。這個函數調用單一block多次,并平行運算,然后等待所有運算結束,就像我們想要的那樣:
- dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_apply([array count], queue, ^(size_t index){
- [self doSomethingIntensiveWith:[array objectAtIndex:index]];
- });
- [self doSomethingWith:array];
這很棒,但是異步咋辦?dispatch_apply函數可是沒有異步版本的。但是我們使用的可是一個為異步而生的API啊!所以我們只要用dispatch_async函數將所有代碼推到后臺就行了:
- dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_async(queue, ^{
- dispatch_apply([array count], queue, ^(size_t index){
- [self doSomethingIntensiveWith:[array objectAtIndex:index]];
- });
- [self doSomethingWith:array];
- });
簡單的要死!
這種方法的關鍵在于確定我們的代碼是在一次對不同的數據片段進行相似的操作。如果你確定你的任務是線程安全的(不在本篇討論范圍內)那么你可以使用GCD來重寫你的循環了,更平行更風騷。
要看到性能提升,你還得進行一大堆工作。比之線程,GCD是輕量和低負載的,但是將block提交至queue還是很消耗資源的——block需要被拷貝和入隊,同時適當的工作線程需要被通知。不要將一張圖片的每個像素作為一個block提交至隊列,GCD的優點就半途夭折了。如果你不確定,那么請進行試驗。將程序平行計算化是一種優化措施,在修改代碼之前你必須再三思索,確定修改是有益的(還有確保你修改了正確的地方)。
Subsystem并發運算
前面的章節我們討論了在程序的單個subsystem中發揮多核心的優勢。下來我們要跨越多個子系統。
例如,設想一個程序要打開一個包含meta信息的文檔。文檔數據本身需要解析并轉換至模型對象來顯示,meta信息也需要解析和轉換。但是,文檔數據和meta信息不需要交互。我們可以為文檔和meta各創建一個dispatch queue,然后并發執行。文檔和meta的解析代碼都會各自串行執行,從而不用考慮線程安全(只要沒有文檔和meta之間共享的數據),但是它們還是并發執行的。
一旦文檔打開了,程序需要響應用戶操作。例如,可能需要進行拼寫檢查、代碼高亮、字數統計、自動保存或者其他什么。如果每個任務都被實現為在不同的dispatch queue中執行,那么這些任務會并發執行,并各自將其他任務的運算考慮在內(respect to each other),從而省去了多線程編程的麻煩。
使用dispatch source(下次我會講到),我們可以讓GCD將事件直接傳遞給用戶隊列。例如,程序中監視socket連接的代碼可以被置于它自己的dispatch queue中,這樣它會異步執行,并且執行時會將程序其他部分的運算考慮在內。另外,如果使用用戶隊列的話,這個模塊會串行執行,簡化程序。
結論
我們討論了如何使用GCD來提升程序性能以及發揮多核系統的優勢。盡管我們需要比較謹慎地編寫并發程序,GCD還是使得我們能更簡單地發揮系統的可用計算資源。
下一篇中,我們將討論dispatch source,也就是GCD的監視內部、外部事件的機制。