天天看點

objc 并發程式設計的相關API

目錄 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。

  1. struct threadInfo { 
  2.     uint32_t * inputValues; 
  3.     size_t count; 
  4. }; 
  5. struct threadResult { 
  6.     uint32_t min; 
  7.     uint32_t max; 
  8. }; 
  9. void * findMinAndMax(void *arg) 
  10.     struct threadInfo const * const info = (struct threadInfo *) arg; 
  11.     uint32_t min = UINT32_MAX; 
  12.     uint32_t max = 0; 
  13.     for (size_t i = 0; i < info-&gt;count; ++i) { 
  14.         uint32_t v = info-&gt;inputValues[i]; 
  15.         min = MIN(min, v); 
  16.         max = MAX(max, v); 
  17.     } 
  18.     free(arg); 
  19.     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result)); 
  20.     result-&gt;min = min; 
  21.     result-&gt;max = max; 
  22.     return result; 
  23. int main(int argc, const char * argv[]) 
  24.     size_t const count = 1000000; 
  25.     uint32_t inputValues[count]; 
  26.     // Fill input values with random numbers: 
  27.     for (size_t i = 0; i < count; ++i) { 
  28.         inputValues[i] = arc4random(); 
  29.     } 
  30.     // Spawn 4 threads to find the minimum and maximum: 
  31.     size_t const threadCount = 4; 
  32.     pthread_t tid[threadCount]; 
  33.     for (size_t i = 0; i < threadCount; ++i) {         struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));         size_t offset = (count / threadCount) * i;         info-&gt;inputValues = inputValues + offset; 
  34.         info-&gt;count = MIN(count - offset, count / threadCount); 
  35.         int err = pthread_create(tid + i, NULL, &amp;findMinAndMax, info); 
  36.         NSCAssert(err == 0, @"pthread_create() failed: %d", err); 
  37.     } 
  38.     // Wait for the threads to exit: 
  39.     struct threadResult * results[threadCount]; 
  40.     for (size_t i = 0; i < threadCount; ++i) { 
  41.         int err = pthread_join(tid[i], (void **) &amp;(results[i])); 
  42.         NSCAssert(err == 0, @"pthread_join() failed: %d", err); 
  43.     } 
  44.     // Find the min and max: 
  45.     uint32_t min = UINT32_MAX; 
  46.     uint32_t max = 0; 
  47.     for (size_t i = 0; i < threadCount; ++i) {         min = MIN(min, results[i]-&gt;min); 
  48.         max = MAX(max, results[i]-&gt;max); 
  49.         free(results[i]); 
  50.         results[i] = NULL; 
  51.     } 
  52.     NSLog(@"min = %u", min); 
  53.     NSLog(@"max = %u", max); 

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

  1. @interface FindMinMaxThread : NSThread 
  2. @property (nonatomic) NSUInteger min; 
  3. @property (nonatomic) NSUInteger max; 
  4. - (instancetype)initWithNumbers:(NSArray *)numbers; 
  5. @end 
  6. @implementation FindMinMaxThread { 
  7.     NSArray *_numbers; 
  8. - (instancetype)initWithNumbers:(NSArray *)numbers  
  9.     self = [super init]; 
  10.     if (self) { 
  11.         _numbers = numbers; 
  12.     } 
  13.     return self; 
  14. - (void)main 
  15.     NSUInteger min; 
  16.     NSUInteger max; 
  17.     // process the data 
  18.     self.min = min; 
  19.     self.max = max; 
  20. @end 

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

  1. NSSet *threads = [NSMutableSet set]; 
  2. NSUInteger numberCount = self.numbers.count; 
  3. NSUInteger threadCount = 4; 
  4. for (NSUInteger i = 0; i < threadCount; i++) { 
  5.     NSUInteger offset = (count / threadCount) * i; 
  6.     NSUInteger count = MIN(numberCount - offset, numberCount / threadCount); 
  7.     NSRange range = NSMakeRange(offset, count); 
  8.     NSArray *subset = [self.numbers subarrayWithRange:range]; 
  9.     FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset]; 
  10.     [threads addObject:thread]; 
  11.     [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都将被放入到系統的線程池的一個全局隊列中。    

objc 并發程式設計的相關API

這裡隊列中,可以使用不同優先級,這聽起來可能非常簡單,不過,強烈建議,在大多數情況下使用預設的優先級就可以了。在隊列中排程具有不同優先級的任務時,如果這些任務需要通路一些共享的資源,可能會迅速引起不可預料到的行為,這樣可能會引起程式的突然停止——運作時,低優先級的任務阻塞了高優先級任務。更多相關内容,在本文的優先級反轉中會有介紹。   雖然GCD是稍微偏底層的一個API,但是使用起來非常的簡單。不過這也容易使開發者忘記并發程式設計中的許多注意事項和陷阱。讀者可以閱讀本文後面的:并發程式設計中面臨的挑戰,這樣可以注意到一些潛在的問題。本期的另外一篇文章: Low-level Concurrency API,給出了更加深入的解釋,以及一些有價值的提示。   2.3、OPERATION QUEUES 操作隊列(operation queue)是基于GCD封裝的一個隊列模型。GCD提供了更加底層的控制,而操作隊列在GCD之上實作了一些友善的功能,這些功能對于開發者來說會更好、更安全。   類NSOperationQueue有兩個不同類型的隊列:主隊列和自定義隊列。主隊列運作在主線程之上,而自定義隊列在背景執行。任何情況下,在這兩種隊列中運作的任務,都是由NSOperation組成。   定義自己的操作有兩種方式:重寫main或者start方法,前一種方法非常簡單,但是靈活性不如後一種。對于重寫main方法來說,開發者不需要管理一些狀态屬性(例如isExecuting和isFinished)——當main傳回的時候,就可以假定操作結束。

  1. @implementation YourOperation 
  2.     - (void)main 
  3.     { 
  4.         // do your work here ... 
  5.     }  
  6. @end 

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

  1. @implementation YourOperation 
  2.     - (void)start 
  3.     { 
  4.         self.isExecuting = YES; 
  5.         self.isFinished = NO; 
  6.         // start your work, which calls finished once it's done ... 
  7.     } 
  8.     - (void)finished 
  9.     { 
  10.         self.isExecuting = NO; 
  11.         self.isFinished = YES; 
  12.     } 
  13. @end 

  注意:這種情況下,需要開發者手動管理操作的狀态。 為了讓操作隊列能夠捕獲到操作的改變,需要将狀态屬性以KVO的方式實作。并確定狀态改變的時候發送了KVO消息。   為了滿足操作隊列提供的取消功能,還應該檢查isCancelled屬性,以判斷是否繼續運作。

  1. - (void)main 
  2.     while (notDone &amp;&amp; !self.isCancelled) { 
  3.         // do your processing 
  4.     } 

  當開發者定義好操作類之後,就可以很容易的将一個操作添加到隊列中:

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
  2. YourOperation *operation = [[YourOperation alloc] init]; 
  3. [queue  addOperation:operation]; 

  另外,開發者也可以将block添加到隊列中。這非常的友善,例如,你希望在主隊列中排程一個一次性任務:

  1. [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
  2.     // do something... 
  3. }]; 

  如果重寫operation的description方法,可以很容易的标示出在某個隊列中目前被排程的所有operation。   除了提供基本的排程操作或block外,操作隊列還提供了一些正确使用GCD的功能。例如,可以通過maxConcurrentOperationCount屬性來控制一個隊列中可以有多少個操作參與并發執行,以及将隊列設定為一個串行隊列。   另外還有一個友善的功能就是根據隊列中operation的優先級對其進行排序,這不同于GCD的隊列優先級,它隻會影響到一個隊列中所有被排程的operation的執行順序。如果你需要進一步控制operation的執行順序(除了使用5個标準的優先級),還可以在operation之間指定依賴,如下:

  1. [intermediateOperation addDependency:operation1]; 
  2. [intermediateOperation addDependency:operation2]; 
  3. [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,都會立即退出。