iOS多執行緒之併發程式設計-4

weixin_34320159發表於2016-11-23

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處理,或者通知應用

相關文章