多線程心得

?iOS 提供了幾種不同的 API 來支持并發編程。每一個 API 都具有不同的功能和使用限制,這使它們適合不同的任務。同時,這些 API 處在不同的抽象層級上。我們有可能用其進行非常深入底層的操作,但是這也意味著背負起將任務進行良好處理的巨大責任。

實際上,并發編程是一個很有挑戰的主題,它有許多錯綜復雜的問題和陷阱。當開發者在使用類似Grand Central Dispatch(GCD)或NSOperationQueue的 API 時,很容易遺忘這些問題和陷阱。本文首先對 OS X 和 iOS 中不同的并發編程 API 進行一些介紹,然后再深入了解并發編程中獨立于與你所使用的特定 API 的一些內在挑戰。


蘋果的移動和桌面操作系統中提供了相同的并發編程API。 pthread、NSThread、GCD、NSOperationQueue,以及NSRunLoop。實際上把 run loop 也列在其中是有點奇怪,因為它并不能實現真正的并行,不過因為它與并發編程有莫大的關系,因此值得我們進行一些深入了解。

由于高層 API 是基于底層 API 構建的,所以我們首先將從底層的 API 開始介紹,然后逐步擴展到高層 API。不過在具體編程中,選擇 API 的順序剛好相反:因為大多數情況下,選擇高層的 API 不僅可以完成底層 API 能完成的任務,而且能夠讓并發模型變得簡單。

如果你對我們為何堅持推薦使用高抽象層級以及簡單的并行代碼有所疑問的話,那么你可以看看這篇文章的第二部分并發編程中面臨的挑戰,以及 Peter Steinberger 寫的關于線程安全的文章。

線程

線程(thread)是組成進程的子單元,操作系統的調度器可以對線程進行單獨的調度。實際上,所有的并發編程 API 都是構建于線程之上的 —— 包括 GCD 和操作隊列(operation queues)。

多線程可以在單核 CPU 上同時(或者至少看作同時)運行。操作系統將小的時間片分配給每一個線程,這樣就能夠讓用戶感覺到有多個任務在同時進行。如果 CPU 是多核的,那么線程就可以真正的以并發方式被執行,從而減少了完成某項操作所需要的總時間。

你可以使用 Instruments 中的CPU strategy view來得知你的代碼或者你在使用的框架代碼是如何在多核 CPU 中調度執行的。

需要重點關注的是,你無法控制你的代碼在什么地方以及什么時候被調度,以及無法控制執行多長時間后將被暫停,以便輪換執行別的任務。這種線程調度是非常強大的一種技術,但是也非常復雜,我們稍后研究。

先把線程調度的復雜情況放一邊,開發者可以使用POSIX 線程API,或者 Objective-C 中提供的對該 API 的封裝NSThread,來創建自己的線程。下面這個小示例利用pthread來在一百萬個數字中查找最小值和最大值。其中并發執行了 4 個線程。從該示例復雜的代碼中,應該可以看出為什么大家不會希望直接使用 pthread 。

#import? <pthread.h>

struct threadInfo { uint32_t * inputValues; size_t count; };t

struct threadResult { uint32_t min; uint32_t max; };

void * findMinAndMax(void *arg)

{ struct threadInfo const * const info = (struct threadInfo *) arg;

uint32_t min = UINT32_MAX;

uint32_t max = 0;

for (size_t i = 0; i < info->count; ++i) {

uint32_t v = info->inputValues[i];

min = MIN(min, v);

max = MAX(max, v);

}

free(arg);

struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));

result->min = min;

result->max = max;

return result;

}

int ?main (intargc,constchar* argv[])

{ size_tconstcount =1000000;?

uint32_t inputValues[count];

// 使用隨機數字填充

?inputValuesfor(size_t i =0; i < count; ++i) {

?inputValues[i] = arc4random();

?}

// 開始4個尋找最小值和最大值的線程

size_tconstthreadCount =4;

?pthread_t tid[threadCount];

for(size_t i =0; i < threadCount; ++i) {structthreadInfo *constinfo = (structthreadInfo *) malloc(sizeof(*info)); size_t offset = (count / threadCount) * i; info->inputValues = inputValues + offset; info->count = MIN(count - offset, count / threadCount);interr = pthread_create(tid + i,NULL, &findMinAndMax, info); NSCAssert(err ==0,@"pthread_create() failed: %d", err); }// 等待線程退出structthreadResult * results[threadCount];for(size_t i =0; i < threadCount; ++i) {interr = pthread_join(tid[i], (void**) &(results[i])); NSCAssert(err ==0,@"pthread_join() failed: %d", err); }// 尋找 min 和 maxuint32_t min = UINT32_MAX; uint32_t max =0;for(size_t i =0; i < threadCount; ++i) { min = MIN(min, results[i]->min); max = MAX(max, results[i]->max); free(results[i]); results[i] =NULL; }NSLog(@"min = %u", min);NSLog(@"max = %u", max);return0; }

NSThread是 Objective-C 對 pthread 的一個封裝。通過封裝,在 Cocoa 環境中,可以讓代碼看起來更加親切。例如,開發者可以利用 NSThread 的一個子類來定義一個線程,在這個子類的中封裝需要在后臺線程運行的代碼。針對上面的那個例子,我們可以定義一個這樣的NSThread子類:

@interfaceFindMinMaxThread:NSThread@property(nonatomic) NSUInteger min;@property(nonatomic) NSUInteger max;- (instancetype)initWithNumbers:(NSArray*)numbers;@end@implementationFindMinMaxThread{NSArray*_numbers;}- (instancetype)initWithNumbers:(NSArray*)numbers{self= [superinit];if(self) {? ? ? ? _numbers = numbers;? ? }returnself;}- (void)main{? ? NSUInteger min;? ? NSUInteger max;// 進行相關數據的處理self.min= min;self.max= max;}@end

要想啟動一個新的線程,需要創建一個線程對象,然后調用它的start方法:

NSMutableSet*threads = [NSMutableSetset];NSUIntegernumberCount =self.numbers.count;NSUIntegerthreadCount =4;for(NSUIntegeri =0; i < threadCount; i++) {NSUIntegeroffset = (count/ threadCount) * i;NSUIntegercount=MIN(numberCount - offset, numberCount / threadCount);NSRangerange =NSMakeRange(offset,count);NSArray*subset = [self.numbers subarrayWithRange:range];FindMinMaxThread*thread = [[FindMinMaxThreadalloc] initWithNumbers:subset];? ? [threads addObject:thread];? ? [thread start];}

現在,我們可以通過檢測到線程的isFinished屬性來檢測新生成的線程是否已經結束,并獲取結果。我們將這個練習留給感興趣的讀者,這主要是因為不論使用pthread還是NSThread來直接對線程操作,都是相對糟糕的編程體驗,這種方式并不適合我們以寫出良好代碼為目標的編碼精神。

直接使用線程可能會引發的一個問題是,如果你的代碼和所基于的框架代碼都創建自己的線程時,那么活動的線程數量有可能以指數級增長。這在大型工程中是一個常見問題。例如,在 8 核 CPU 中,你創建了 8 個線程來完全發揮 CPU 性能。然而在這些線程中你的代碼所調用的框架代碼也做了同樣事情(因為它并不知道你已經創建的這些線程),這樣會很快產生成成百上千的線程。代碼的每個部分自身都沒有問題,然而最后卻還是導致了問題。使用線程并不是沒有代價的,每個線程都會消耗一些內存和內核資源。

接下來,我們將介紹兩個基于隊列的并發編程 API :GCD 和 operation queue 。它們通過集中管理一個被大家協同使用的線程池,來解決上面遇到的問題。

Grand Central Dispatch

為了讓開發者更加容易的使用設備上的多核CPU,蘋果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD)。在下一篇關于底層并發 API的文章中,我們將更深入地介紹 GCD。

通過 GCD,開發者不用再直接跟線程打交道了,只需要向隊列中添加代碼塊即可,GCD 在后端管理著一個線程池。GCD 不僅決定著你的代碼塊將在哪個線程被執行,它還根據可用的系統資源對這些線程進行管理。這樣可以將開發者從線程管理的工作中解放出來,通過集中的管理線程,來緩解大量線程被創建的問題。

GCD 帶來的另一個重要改變是,作為開發者可以將工作考慮為一個隊列,而不是一堆線程,這種并行的抽象模型更容易掌握和使用。

GCD 公開有 5 個不同的隊列:運行在主線程中的 main queue,3 個不同優先級的后臺隊列,以及一個優先級更低的后臺隊列(用于 I/O)。

另外,開發者可以創建自定義隊列:串行或者并行隊列。自定義隊列非常強大,在自定義隊列中被調度的所有 block 最終都將被放入到系統的全局隊列中和線程池中。

使用不同優先級的若干個隊列乍聽起來非常直接,不過,我們強烈建議,在絕大多數情況下使用默認的優先級隊列就可以了。如果執行的任務需要訪問一些共享的資源,那么在不同優先級的隊列中調度這些任務很快就會造成不可預期的行為。這樣可能會引起程序的完全掛起,因為低優先級的任務阻塞了高優先級任務,使它不能被執行。更多相關內容,在本文的優先級反轉部分中會有介紹。

雖然 GCD 是一個低層級的 C API ,但是它使用起來非常的直接。不過這也容易使開發者忘記并發編程中的許多注意事項和陷阱。讀者可以閱讀本文后面的并發編程中面臨的挑戰,這樣可以注意到一些潛在的問題。本期的另外一篇優秀文章:底層并發 API中,包含了很多深入的解釋和一些有價值的提示。

Operation Queues

操作隊列(operation queue)是由 GCD 提供的一個隊列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操作隊列則在 GCD 之上實現了一些方便的功能,這些功能對于 app 的開發者來說通常是最好最安全的選擇。

NSOperationQueue有兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在后臺執行。在兩種類型中,這些隊列所處理的任務都使用NSOperation的子類來表述。

你可以通過重寫main或者start方法 來定義自己的operations。前一種方法非常簡單,開發者不需要管理一些狀態屬性(例如isExecuting和isFinished),當main方法返回的時候,這個 operation 就結束了。這種方式使用起來非常簡單,但是靈活性相對重寫start來說要少一些。

@implementationYourOperation- (void)main? ? {// 進行處理 ...}@end

如果你希望擁有更多的控制權,以及在一個操作中可以執行異步任務,那么就重寫start方法:

@implementationYourOperation- (void)start? ? {self.isExecuting=YES;self.isFinished=NO;// 開始處理,在結束時應該調用 finished ...}? ? - (void)finished? ? {self.isExecuting=NO;self.isFinished=YES;? ? }@end

注意:這種情況下,你必須手動管理操作的狀態。 為了讓操作隊列能夠捕獲到操作的改變,需要將狀態的屬性以配合 KVO 的方式進行實現。如果你不使用它們默認的 setter 來進行設置的話,你就需要在合適的時候發送合適的 KVO 消息。

為了能使用操作隊列所提供的取消功能,你需要在長時間操作中時不時地檢查isCancelled屬性:

- (void)main{while(notDone && !self.isCancelled) {// 進行處理}}

當你定義好 operation 類之后,就可以很容易的將一個 operation 添加到隊列中:

NSOperationQueue*queue = [[NSOperationQueuealloc]init];YourOperation*operation = [[YourOperationalloc]init];[queue? addOperation:operation];

另外,你也可以將 block 添加到操作隊列中。這有時候會非常的方便,比如你希望在主隊列中調度一個一次性任務:

[[NSOperationQueuemainQueue]addOperationWithBlock:^{//代碼...}];

雖然通過這種的方式在隊列中添加操作會非常方便,但是定義你自己的 NSOperation 子類會在調試時很有幫助。如果你重寫 operation 的description方法,就可以很容易的標示出在某個隊列中當前被調度的所有操作 。

除了提供基本的調度操作或 block 外,操作隊列還提供了在 GCD 中不太容易處理好的特性的功能。例如,你可以通過maxConcurrentOperationCount屬性來控制一個特定隊列中可以有多少個操作參與并發執行。將其設置為 1 的話,你將得到一個串行隊列,這在以隔離為目的的時候會很有用。

另外還有一個方便的功能就是根據隊列中operation的優先級對其進行排序,這不同于 GCD 的隊列優先級,它只影響當前隊列中所有被調度的 operation 的執行先后。如果你需要進一步在除了 5 個標準的優先級以外對 operation 的執行順序進行控制的話,還可以在 operation 之間指定依賴關系,如下:

[intermediateOperation addDependency:operation1];

[intermediateOperation addDependency:operation2];

[finishedOperation addDependency:intermediateOperation];

這些簡單的代碼可以確保operation1和operation2在intermediateOperation之前執行,當然,也會在finishOperation之前被執行。對于需要明確的執行順序時,操作依賴是非常強大的一個機制。它可以讓你創建一些操作組,并確保這些操作組在依賴它們的操作被執行之前執行,或者在并發隊列中以串行的方式執行操作。

從本質上來看,操作隊列的性能比 GCD 要低那么一點,不過,大多數情況下這點負面影響可以忽略不計,操作隊列是并發編程的首選工具。

Run Loops

實際上,Run loop并不像 GCD 或者操作隊列那樣是一種并發機制,因為它并不能并行執行任務。不過在主 dispatch/operation 隊列中, run loop 將直接配合任務的執行,它提供了一種異步執行代碼的機制。

Run loop 比起操作隊列或者 GCD 來說容易使用得多,因為通過 run loop ,你不必處理并發中的復雜情況,就能異步地執行任務。

一個 run loop 總是綁定到某個特定的線程中。main run loop 是與主線程相關的,在每一個 Cocoa 和 CocoaTouch 程序中,這個 main run loop 都扮演了一個核心角色,它負責處理 UI 事件、計時器,以及其它內核相關事件。無論你什么時候設置計時器、使用NSURLConnection或者調用performSelector:withObject:afterDelay:,其實背后都是 run loop 在處理這些異步任務。

無論何時你使用 run loop 來執行一個方法的時候,都需要記住一點:run loop 可以運行在不同的模式中,每種模式都定義了一組事件,供 run loop 做出響應。這在對應 main run loop 中暫時性的將某個任務優先執行這種任務上是一種聰明的做法。

關于這點,在 iOS 中非常典型的一個示例就是滾動。在進行滾動時,run loop 并不是運行在默認模式中的,因此, run loop 此時并不會響應比如滾動前設置的計時器。一旦滾動停止了,run loop 會回到默認模式,并執行添加到隊列中的相關事件。如果在滾動時,希望計時器能被觸發,需要將其設為NSRunLoopCommonModes的模式,并添加到 run loop 中。

主線程一般來說都已經配置好了 main run loop。然而其他線程默認情況下都沒有設置 run loop。你也可以自行為其他線程設置 run loop ,但是一般來說我們很少需要這么做。大多數時間使用 main run loop 會容易得多。如果你需要處理一些很重的工作,但是又不想在主線程里做,你仍然可以在你的代碼在 main run loop 中被調用后將工作分配給其他隊列。Chris 在他關于常見的后臺實踐的文章里闡述了一些關于這種模式的很好的例子。

如果你真需要在別的線程中添加一個 run loop ,那么不要忘記在 run loop 中至少添加一個 input source 。如果 run loop 中沒有設置好的 input source,那么每次運行這個 run loop ,它都會立即退出。

并發編程中面臨的挑戰

使用并發編程會帶來許多陷阱。只要一旦你做的事情超過了最基本的情況,對于并發執行的多任務之間的相互影響的不同狀態的監視就會變得異常困難。 問題往往發生在一些不確定性(不可預見性)的地方,這使得在調試相關并發代碼時更加困難。

關于并發編程的不可預見性有一個非常有名的例子:在1995年, NASA (美國宇航局)發送了開拓者號火星探測器,但是當探測器成功著陸在我們紅色的鄰居星球后不久,任務嘎然而止,火星探測器莫名其妙的不停重啟,在計算機領域內,遇到的這種現象被定為為優先級反轉,也就是說低優先級的線程一直阻塞著高優先級的線程。稍后我們會看到關于這個問題的更多細節。在這里我們想說明的是,即使擁有豐富的資源和大量優秀工程師的智慧,并發也還是會在不少情況下反咬你你一口。

資源共享

并發編程中許多問題的根源就是在多線程中訪問共享資源。資源可以是一個屬性、一個對象,通用的內存、網絡設備或者一個文件等等。在多線程中任何一個共享的資源都可能是一個潛在的沖突點,你必須精心設計以防止這種沖突的發生。

為了演示這類問題,我們舉一個關于資源的簡單示例:比如僅僅用一個整型值來做計數器。在程序運行過程中,我們有兩個并行線程 A 和 B,這兩個線程都嘗試著同時增加計數器的值。問題來了,你通過 C 語言或 Objective-C 寫的代碼大多數情況下對于 CPU 來說不會僅僅是一條機器指令。要想增加計數器的值,當前的必須被從內存中讀出,然后增加計數器的值,最后還需要將這個增加后的值寫回內存中。

我們可以試著想一下,如果兩個線程同時做上面涉及到的操作,會發生怎樣的偶然。例如,線程 A 和 B 都從內存中讀取出了計數器的值,假設為17,然后線程A將計數器的值加1,并將結果18寫回到內存中。同時,線程B也將計數器的值加 1 ,并將結果18寫回到內存中。實際上,此時計數器的值已經被破壞掉了,因為計數器的值17被加 1 了兩次,而它的值卻是18。

這個問題被叫做競態條件,在多線程里面訪問一個共享的資源,如果沒有一種機制來確保在線程 A 結束訪問一個共享資源之前,線程 B 就不會開始訪問該共享資源的話,資源競爭的問題就總是會發生。如果你所寫入內存的并不是一個簡單的整數,而是一個更復雜的數據結構,可能會發生這樣的現象:當第一個線程正在寫入這個數據結構時,第二個線程卻嘗試讀取這個數據結構,那么獲取到的數據可能是新舊參半或者沒有初始化。為了防止出現這樣的問題,多線程需要一種互斥的機制來訪問共享資源。

在實際的開發中,情況甚至要比上面介紹的更加復雜,因為現代 CPU 為了優化目的,往往會改變向內存讀寫數據的順序(亂序執行)。

互斥鎖

互斥訪問的意思就是同一時刻,只允許一個線程訪問某個特定資源。為了保證這一點,每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖,一旦某個線程對資源完成了操作,就釋放掉這個互斥鎖,這樣別的線程就有機會訪問該共享資源了。

除了確?;コ庠L問,還需要解決代碼無序執行所帶來的問題。如果不能確保 CPU 訪問內存的順序跟編程時的代碼指令一樣,那么僅僅依靠互斥訪問是不夠的。為了解決由 CPU 的優化策略引起的副作用,還需要引入內存屏障。通過設置內存屏障,來確保沒有無序執行的指令能跨過屏障而執行。

當然,互斥鎖自身的實現是需要沒有競爭條件的。這實際上是非常重要的一個保證,并且需要在現代 CPU 上使用特殊的指令。更多關于原子操作(atomic operation)的信息,請閱讀 Daniel 寫的文章:底層并發技術。

從語言層面來說,在 Objective-C 中將屬性以 atomic 的形式來聲明,就能支持互斥鎖了。事實上在默認情況下,屬性就是 atomic 的。將一個屬性聲明為 atomic 表示每次訪問該屬性都會進行隱式的加鎖和解鎖操作。雖然最把穩的做法就是將所有的屬性都聲明為 atomic,但是加解鎖這也會付出一定的代價。

在資源上的加鎖會引發一定的性能代價。獲取鎖和釋放鎖的操作本身也需要沒有競態條件,這在多核系統中是很重要的。另外,在獲取鎖的時候,線程有時候需要等待,因為可能其它的線程已經獲取過資源的鎖了。這種情況下,線程會進入休眠狀態。當其它線程釋放掉相關資源的鎖時,休眠的線程會得到通知。所有這些相關操作都是非常昂貴且復雜的。

鎖也有不同的類型。當沒有競爭時,有些鎖在沒有鎖競爭的情況下性能很好,但是在有鎖的競爭情況下,性能就會大打折扣。另外一些鎖則在基本層面上就比較耗費資源,但是在競爭情況下,性能的惡化會沒那么厲害。(鎖的競爭是這樣產生的:當一個或者多個線程嘗試獲取一個已經被別的線程獲取過了的鎖)。

在這里有一個東西需要進行權衡:獲取和釋放鎖所是要帶來開銷的,因此你需要確保你不會頻繁地進入和退出臨界區段(比如獲取和釋放鎖)。同時,如果你獲取鎖之后要執行一大段代碼,這將帶來鎖競爭的風險:其它線程可能必須等待獲取資源鎖而無法工作。這并不是一項容易解決的任務。

我們經常能看到本來計劃并行運行的代碼,但實際上由于共享資源中配置了相關的鎖,所以同一時間只有一個線程是處于激活狀態的。對于你的代碼會如何在多核上運行的預測往往十分重要,你可以使用 Instrument 的CPU strategy view來檢查是否有效的利用了 CPU 的可用核數,進而得出更好的想法,以此來優化代碼。

死鎖

互斥鎖解決了競態條件的問題,但很不幸同時這也引入了一些其他問題,其中一個就是死鎖。當多個線程在相互等待著對方的結束時,就會發生死鎖,這時程序可能會被卡住。

看看下面的代碼,它交換兩個變量的值:

voidswap(A, B){? ? lock(lockA);? ? lock(lockB);inta = A;intb = B;? ? A = b;? ? B = a;? ? unlock(lockB);? ? unlock(lockA);}

大多數時候,這能夠正常運行。但是當兩個線程使用相反的值來同時調用上面這個方法時:

swap(X,Y);// 線程 1swap(Y,X);// 線程 2

此時程序可能會由于死鎖而被終止。線程 1 獲得了 X 的一個鎖,線程 2 獲得了 Y 的一個鎖。 接著它們會同時等待另外一把鎖,但是永遠都不會獲得。

再說一次,你在線程之間共享的資源越多,你使用的鎖也就越多,同時程序被死鎖的概率也會變大。這也是為什么我們需要盡量減少線程間資源共享,并確保共享的資源盡量簡單的原因之一。建議閱讀一下底層并發編程 API中的全部使用異步分發一節。

資源饑餓(Starvation)

當你認為已經足夠了解并發編程面臨的問題時,又出現了一個新的問題。鎖定的共享資源會引起讀寫問題。大多數情況下,限制資源一次只能有一個線程進行讀取訪問其實是非常浪費的。因此,在資源上沒有寫入鎖的時候,持有一個讀取鎖是被允許的。這種情況下,如果一個持有讀取鎖的線程在等待獲取寫入鎖的時候,其他希望讀取資源的線程則因為無法獲得這個讀取鎖而導致資源饑餓的發生。

為了解決這個問題,我們需要使用一個比簡單的讀/寫鎖更聰明的方法,例如給定一個writer preference,或者使用read-copy-update算法。Daniel 在底層并發編程 API中有介紹了如何用 GCD 實現一個多讀取單寫入的模式,這樣就不會被寫入資源饑餓的問題困擾了。

優先級反轉

本節開頭介紹了美國宇航局發射的開拓者號火星探測器在火星上遇到的并發問題?,F在我們就來看看為什么開拓者號幾近失敗,以及為什么有時候我們的程序也會遇到相同的問題,該死的優先級反轉

優先級反轉是指程序在運行時低優先級的任務阻塞了高優先級的任務,有效的反轉了任務的優先級。由于 GCD 提供了擁有不同優先級的后臺隊列,甚至包括一個 I/O 隊列,所以我們最好了解一下優先級反轉的可能性。

高優先級和低優先級的任務之間共享資源時,就可能發生優先級反轉。當低優先級的任務獲得了共享資源的鎖時,該任務應該迅速完成,并釋放掉鎖,這樣高優先級的任務就可以在沒有明顯延時的情況下繼續執行。然而高優先級任務會在低優先級的任務持有鎖的期間被阻塞。如果這時候有一個中優先級的任務(該任務不需要那個共享資源),那么它就有可能會搶占低優先級任務而被執行,因為此時高優先級任務是被阻塞的,所以中優先級任務是目前所有可運行任務中優先級最高的。此時,中優先級任務就會阻塞著低優先級任務,導致低優先級任務不能釋放掉鎖,這也就會引起高優先級任務一直在等待鎖的釋放。

在你的實際代碼中,可能不會像發生在火星的事情那樣戲劇性地不停重啟。遇到優先級反轉時,一般沒那么嚴重。

解決這個問題的方法,通常就是不要使用不同的優先級。通常最后你都會以讓高優先級的代碼等待低優先級的代碼來解決問題。當你使用 GCD 時,總是使用默認的優先級隊列(直接使用,或者作為目標隊列)。如果你使用不同的優先級,很可能實際情況會讓事情變得更糟糕。

從中得到的教訓是,使用不同優先級的多個隊列聽起來雖然不錯,但畢竟是紙上談兵。它將讓本來就復雜的并行編程變得更加復雜和不可預見。如果你在編程中,遇到高優先級的任務突然沒理由地卡住了,可能你會想起本文,以及那個美國宇航局的工程師也遇到過的被稱為優先級反轉的問題。

總結

我們希望通過本文你能夠了解到并發編程帶來的復雜性和相關問題。并發編程中,無論是看起來多么簡單的 API ,它們所能產生的問題會變得非常的難以觀測,而且要想調試這類問題往往也都是非常困難的。

但另一方面,并發實際上是一個非常棒的工具。它充分利用了現代多核 CPU 的強大計算能力。在開發中,關鍵的一點就是盡量讓并發模型保持簡單,這樣可以限制所需要的鎖的數量。

我們建議采納的安全模式是這樣的:從主線程中提取出要使用到的數據,并利用一個操作隊列在后臺處理相關的數據,最后回到主隊列中來發送你在后臺隊列中得到的結果。使用這種方式,你不需要自己做任何鎖操作,這也就大大減少了犯錯誤的幾率。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Object C中創建線程的方法是什么?如果在主線程中執行代碼,方法是什么?如果想延時執行代碼、方法又是什么? 1...
    AlanGe閱讀 1,793評論 0 17
  • 并發所描述的概念就是同時運行多個任務。這些任務可能是以在單核 CPU 上分時(時間共享)的形式同時運行,也可能是在...
    問題餓閱讀 384評論 0 3
  • 本文將從以下幾個部分來介紹多線程。 第一部分介紹多線程的基本原理。 第二部分介紹Run loop。 第三部分介紹多...
    曲年閱讀 1,287評論 2 14
  • 由于文章長度限制,本文作為[譯]線程編程指南(一)后續部分。 Run Loops Run loop是與線程相關的基...
    巧巧的二表哥閱讀 1,225評論 0 5
  • 卷首語 歡迎來到 objc.io 的第二期! 首先,我們想對各位讀者說聲謝謝!第一期推出后,大家的反饋如同潮水一般...
    評評分分閱讀 447評論 0 7