iOS多執行緒之併發程式設計-4
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處理,或者通知應用
相關文章
- 併發程式設計之:執行緒程式設計執行緒
- iOS開發-多執行緒程式設計iOS執行緒程式設計
- 多執行緒併發程式設計“鎖”事執行緒程式設計
- 併發程式設計之:執行緒池(一)程式設計執行緒
- Java併發程式設計之執行緒安全、執行緒通訊Java程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒中斷(三)Java程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒簡介(二)Java程式設計執行緒
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- 併發程式設計之:深入解析執行緒池程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒的由來(一)Java程式設計執行緒
- 【多執行緒高併發程式設計】二 實現多執行緒的幾種方式執行緒程式設計
- 併發程式設計之volatile與JMM多執行緒記憶體模型程式設計執行緒記憶體模型
- Python併發程式設計之建立多執行緒的幾種方法(二)Python程式設計執行緒
- 併發程式設計之 執行緒協作工具類程式設計執行緒
- Java併發程式設計序列之執行緒狀態Java程式設計執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- C++11併發程式設計:多執行緒std::threadC++程式設計執行緒thread
- 併發程式設計與執行緒安全程式設計執行緒
- Java併發程式設計:Java執行緒Java程式設計執行緒
- 從執行緒到併發程式設計執行緒程式設計
- java併發程式設計——執行緒池Java程式設計執行緒
- java併發程式設計——執行緒同步Java程式設計執行緒
- iOS 多執行緒之執行緒安全iOS執行緒
- iOS多執行緒程式設計三:Operation和OperationQueueiOS執行緒程式設計
- 多執行緒高併發程式設計(10) -- ConcurrentHashMap原始碼分析執行緒程式設計HashMap原始碼
- 程式設計體系結構(05):Java多執行緒併發程式設計Java執行緒
- 併發程式設計——如何終止執行緒程式設計執行緒
- Java併發程式設計-執行緒基礎Java程式設計執行緒
- Java併發程式設計:執行緒池ThreadPoolExecutorJava程式設計執行緒thread
- 併發程式設計之多執行緒基礎程式設計執行緒
- java併發程式設計 | 執行緒詳解Java程式設計執行緒
- C語言 之 多執行緒程式設計C語言執行緒程式設計
- 併發程式設計系列之如何正確使用執行緒池?程式設計執行緒
- Java多執行緒與併發之ThreadLocalJava執行緒thread
- IOS多執行緒之(GCD)iOS執行緒GC
- iOS 多執行緒之GCDiOS執行緒GC
- iOS 多執行緒之NSOperationiOS執行緒
- iOS 多執行緒之NSThreadiOS執行緒thread
- iOS 多執行緒之NSOperationQueueiOS執行緒