目錄 1、介紹 2、OS X和iOS中的并發程式設計 2.1、Threads 2.2、Grand Central Dispatch 2.3、Operation Queues 2.4、Run Loops 3、并發程式設計中面臨的挑戰 3.1、資源共享 3.2、互斥 3.3、死鎖 3.4、饑餓 3.5、優先級反轉 4、小結 正文 1、介紹 并發的意思就是同時運作多個任務,這些任務可以在單核CPU上以 分時(時間共享)的形式同時運作,或者在多核CPU上以真正的并行來運作多任務。 OS X和iOS提供了幾種不同的API來支援并發程式設計。每種API都具有不同的功能和一些限制,一般是根據不同的任務使用不同的API。這些API在系統中處于不同的地方。并發程式設計對于開發者來說非常的強大,但是作為開發者需要擔負很大的責任,來把任務處理好。 實際上,并發程式設計是一個很有挑戰的主題,它有許多錯綜複雜的問題和陷阱,當開發者在使用類似 GCD或 NSOperationQueue API時,很容易遺忘這些問題和陷阱。本文首先介紹一下OS X和iOS中不同的并發程式設計API,然後深入了解并發程式設計中開發者需要面臨的一些挑戰。 2、OS X和iOS中的并發程式設計 在移動和桌面作業系統中,蘋果提供了相同的并發程式設計API。 本文會介紹 pthread和NSThread、Grand Central Dispatch(GCD)、NSOperationQueue,以及NSRunLoop。NSRunLoop列在其中,有點奇怪,因為它并沒有被用來實作真正的并發,不過NSRunLoop與并發程式設計有莫大的關系,值得我們去了解。 由于高層API是基于底層API建構的,是以首先将從底層的API開始介紹,然後逐漸介紹高層API,不過在具體程式設計中,選擇API的順序剛好相反:因為大多數情況下,選擇高層的API不僅可以完成底層API能完成的任務,而且能夠讓并發模型變得簡單。 如果你對這裡給出的建議(API的選擇)上有所顧慮,那麼你可以看看本文的相關内容: 并發程式設計面臨的挑戰,以及Peter Steinberger寫的關于 線程安全的文章。 2.1、THREADS 線程(thread)是組成程序的子單元,作業系統的排程器可以對線程進行單獨的排程。實際上,所有的并發程式設計API都是建構于線程之上的——包括GCD和操作隊列(operation queues)。 多線程可以在單核CPU上同時運作(可以了解為同一時間)——作業系統将時間片配置設定給每一個線程,這樣就能夠讓使用者感覺到有多個任務在同時進行。如果CPU是多核的,那麼線程就可以真正的以并發方式被執行,是以完成某項操作,需要的總時間更少。 開發者可以通過Instrument中的 CPU strategy view來觀察代碼被執行時在多核CPU中的排程情況。 需要重點關注的一件事:開發者無法控制代碼在什麼地方以及什麼時候被排程,以及無法控制代碼執行多長時間後将被暫停,以便輪到執行别的任務。線程排程是非常強大的一種技術,但是也非常複雜(稍後會看到)。 先把線程排程的複雜情況放一邊,開發者可以使用 POSIX線程API,或者Objective-C中提供的對該API的封裝—— NSThread,來建立自己的線程。下面這個小示例是利用 pthread來查找在一百萬個數字中的最小值和最大值。其中并發執行了4個線程。從該示例複雜的代碼中,可以看出為什麼我們不希望直接使用pthread。
- struct threadInfo {
- uint32_t * inputValues;
- size_t count;
- };
- 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(int argc, const char * argv[])
- {
- size_t const count = 1000000;
- uint32_t inputValues[count];
- // Fill input values with random numbers:
- for (size_t i = 0; i < count; ++i) {
- inputValues[i] = arc4random();
- }
- // Spawn 4 threads to find the minimum and maximum:
- size_t const threadCount = 4;
- pthread_t tid[threadCount];
- for (size_t i = 0; i < threadCount; ++i) { struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info)); size_t offset = (count / threadCount) * i; info->inputValues = inputValues + offset;
- info->count = MIN(count - offset, count / threadCount);
- int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
- NSCAssert(err == 0, @"pthread_create() failed: %d", err);
- }
- // Wait for the threads to exit:
- struct threadResult * results[threadCount];
- for (size_t i = 0; i < threadCount; ++i) {
- int err = pthread_join(tid[i], (void **) &(results[i]));
- NSCAssert(err == 0, @"pthread_join() failed: %d", err);
- }
- // Find the min and max:
- uint32_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);
NSThread是Objective-C對 pthread的一個封裝。通過封裝,在Cocoa環境中,可以讓代碼看起來更加親切。例如,開發者可以利用NSThread的一個子類來定義一個線程,在這個子類的中封裝了需要運作的代碼。針對上面的那個例子,我們可以定義一個這樣的NSThread子類:
- @interface FindMinMaxThread : NSThread
- @property (nonatomic) NSUInteger min;
- @property (nonatomic) NSUInteger max;
- - (instancetype)initWithNumbers:(NSArray *)numbers;
- @end
- @implementation FindMinMaxThread {
- NSArray *_numbers;
- }
- - (instancetype)initWithNumbers:(NSArray *)numbers
- {
- self = [super init];
- if (self) {
- _numbers = numbers;
- }
- return self;
- }
- - (void)main
- {
- NSUInteger min;
- NSUInteger max;
- // process the data
- self.min = min;
- self.max = max;
- }
- @end
要想啟動一個新的線程,需要建立一個線程對象,然後調用它的start方法:
- NSSet *threads = [NSMutableSet set];
- NSUInteger numberCount = self.numbers.count;
- NSUInteger threadCount = 4;
- for (NSUInteger i = 0; i < threadCount; i++) {
- NSUInteger offset = (count / threadCount) * i;
- NSUInteger count = MIN(numberCount - offset, numberCount / threadCount);
- NSRange range = NSMakeRange(offset, count);
- NSArray *subset = [self.numbers subarrayWithRange:range];
- FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
- [threads addObject:thread];
- [thread start];
- }
現在,當4個線程結束的時候,我們檢測到線程的isFinished屬性。不過最好還是遠離上面的代碼吧——最主要的原因是,在程式設計中,直接使用線程(無論是pthread,還是NSThread)都是難以接受的。 使用線程會引發的一個問題就是:在開發者自己的代碼,或者系統内部的架構代碼中,被激活的線程數量很有可能會成倍的增加——這對于一個大型工程來說,是很常見的。例如,在8核CPU中,你建立了8個線程,然後在這些線程中調用了架構代碼,這些代碼也建立了同樣的線程(其實它并不知道你已經建立好線程了),這樣會很快産生成千上萬個線程,最終導緻你的程式被終止執行——線程實際上并不是免費的咖啡,每個線程的建立都會消耗一些内容,以及相關的核心資源。 下面,我将介紹兩個基于隊列的并發程式設計API:GCD和operation queue。它們通過集中管理一個線程池(被沒一個任務協同使用),來解決上面遇到的問題。 2.2、Grand Central Dispatch 為了讓開發者更加容易的使用裝置上的多核CPU,蘋果在OS X和iOS 4中引入了Grand Central Dispatch(GCD)。在下一篇文章中會更加詳細的介紹GCD: low-level concurrency APIs。 通過GCD,開發者不用再直接跟線程打交道了,隻需要向隊列中添加block代碼即可,GCD在後端管理着一個線程池。GCD不僅決定着哪個線程(block)将被執行,它還根據可用的系統資源對線程池中的線程進行管理——這樣可以不通過開發者來集中管理線程,緩解大量線程的建立,做到了讓開發者遠離線程的管理。 預設情況下,GCD公開有5個不同的隊列:運作在主線程中的main queue,3個不同優先級的背景隊列,以及一個優先級更低的背景隊列(用于I/O)。另外,開發者可以建立自定義隊列:串行或者并行隊列。自定義隊列非常強大,在自定義隊列中被排程的所有block都将被放入到系統的線程池的一個全局隊列中。

這裡隊列中,可以使用不同優先級,這聽起來可能非常簡單,不過,強烈建議,在大多數情況下使用預設的優先級就可以了。在隊列中排程具有不同優先級的任務時,如果這些任務需要通路一些共享的資源,可能會迅速引起不可預料到的行為,這樣可能會引起程式的突然停止——運作時,低優先級的任務阻塞了高優先級任務。更多相關内容,在本文的優先級反轉中會有介紹。 雖然GCD是稍微偏底層的一個API,但是使用起來非常的簡單。不過這也容易使開發者忘記并發程式設計中的許多注意事項和陷阱。讀者可以閱讀本文後面的:并發程式設計中面臨的挑戰,這樣可以注意到一些潛在的問題。本期的另外一篇文章: Low-level Concurrency API,給出了更加深入的解釋,以及一些有價值的提示。 2.3、OPERATION QUEUES 操作隊列(operation queue)是基于GCD封裝的一個隊列模型。GCD提供了更加底層的控制,而操作隊列在GCD之上實作了一些友善的功能,這些功能對于開發者來說會更好、更安全。 類NSOperationQueue有兩個不同類型的隊列:主隊列和自定義隊列。主隊列運作在主線程之上,而自定義隊列在背景執行。任何情況下,在這兩種隊列中運作的任務,都是由NSOperation組成。 定義自己的操作有兩種方式:重寫main或者start方法,前一種方法非常簡單,但是靈活性不如後一種。對于重寫main方法來說,開發者不需要管理一些狀态屬性(例如isExecuting和isFinished)——當main傳回的時候,就可以假定操作結束。
- @implementation YourOperation
- - (void)main
- {
- // do your work here ...
- }
- @end
如果你希望擁有更多的控制權,以及在一個操作中可以執行異步任務,那麼就重寫start方法:
- @implementation YourOperation
- - (void)start
- {
- self.isExecuting = YES;
- self.isFinished = NO;
- // start your work, which calls finished once it's done ...
- }
- - (void)finished
- {
- self.isExecuting = NO;
- self.isFinished = YES;
- }
- @end
注意:這種情況下,需要開發者手動管理操作的狀态。 為了讓操作隊列能夠捕獲到操作的改變,需要将狀态屬性以KVO的方式實作。并確定狀态改變的時候發送了KVO消息。 為了滿足操作隊列提供的取消功能,還應該檢查isCancelled屬性,以判斷是否繼續運作。
- - (void)main
- {
- while (notDone && !self.isCancelled) {
- // do your processing
- }
- }
當開發者定義好操作類之後,就可以很容易的将一個操作添加到隊列中:
- NSOperationQueue *queue = [[NSOperationQueue alloc] init];
- YourOperation *operation = [[YourOperation alloc] init];
- [queue addOperation:operation];
另外,開發者也可以将block添加到隊列中。這非常的友善,例如,你希望在主隊列中排程一個一次性任務:
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- // do something...
- }];
如果重寫operation的description方法,可以很容易的标示出在某個隊列中目前被排程的所有operation。 除了提供基本的排程操作或block外,操作隊列還提供了一些正确使用GCD的功能。例如,可以通過maxConcurrentOperationCount屬性來控制一個隊列中可以有多少個操作參與并發執行,以及将隊列設定為一個串行隊列。 另外還有一個友善的功能就是根據隊列中operation的優先級對其進行排序,這不同于GCD的隊列優先級,它隻會影響到一個隊列中所有被排程的operation的執行順序。如果你需要進一步控制operation的執行順序(除了使用5個标準的優先級),還可以在operation之間指定依賴,如下:
- [intermediateOperation addDependency:operation1];
- [intermediateOperation addDependency:operation2];
- [finishedOperation addDependency:intermediateOperation];
上面的代碼可以確定operation1和operation在intermediateOperation之前執行,也就是說,在finishOperation之前被執行。對于需要明确的執行順序時,操作依賴是非常強大的一個機制。 它可以讓你建立一些操作組,并確定這些操作組在所依賴的操作之前被執行,或者在并發隊列中以串行的方式執行operation。 從本質上來看,操作隊列的性能比GCD要低,不過,大多數情況下,可以忽略不計,是以操作隊列是并發程式設計的首選API。 2.4、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中。 其實,預設情況下,主線程中總是有一個run loop在運作着,而其它的線程預設情況下,不會有run loop。開發者可以自行為其它的線程添加run loop,隻不過很少需要這樣做。大多數時候,使用main run loop更加友善。如果有大量的任務不希望在主線程中執行,你可以将其派發到别的隊列中。相關内容,Chris寫了一篇文章,可以去看看: common background practices。 如果你真需要在别的線程中添加一個run loop,那麼不要忘記在run loop中至少添加一個input source。如果run loop中沒有input source,那麼每次運作這個run loop,都會立即退出。