iOS併發編程指南(4)

Migrating Away from Threads 編程

從現有的線程代碼遷移到Grand Central Dispatch和Operation對象有許多方法,儘管可能不是全部線程代碼都可以執行遷移,可是遷移可能提高性能,並簡化你的代碼。 安全

使用dispatch queue和Operaiton queue相比線程擁有許多優勢: 網絡

應用再也不須要存儲線程棧到內存空間 數據結構

消除了建立和配置線程的代碼 併發

消除了管理和調度線程工做的代碼 app

簡化了你要編寫的代碼 異步

使用Dispatch Queue替代線程 socket

首先考慮應用可能使用線程的幾種方式: async

單一任務線程:建立一個線程執行單一任務,任務完成時釋放線程 ide

工做線程(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處理,或者通知應用

相關文章
相關標籤/搜索