1. 程式人生 > >IOS_多執行緒程式設計4

IOS_多執行緒程式設計4

一、簡介

在iOS所有實現多執行緒的方案中,GCD應該是最有魅力的,因為GCD本身是蘋果公司為多核的並行運算提出的解決方案。GCD在工作時會自動利用更多的處理器核心,以充分利用更強大的機器。GCD是Grand Central Dispatch的簡稱,它是基於C語言的。如果使用GCD,完全由系統管理執行緒,我們不需要編寫執行緒程式碼。只需定義想要執行的任務,然後新增到適當的排程佇列(dispatch queue)。GCD會負責建立執行緒和排程你的任務,系統直接提供執行緒管理

二、排程佇列(dispath queue)

1.GCD的一個重要概念是佇列,它的核心理念:將長期執行的任務拆分成多個工作單元,並將這些單元新增到dispath queue中,系統會為我們管理這些dispath queue,為我們在多個執行緒上執行工作單元,我們不需要直接啟動和管理後臺執行緒。

2.系統提供了許多預定義的dispath queue,包括可以保證始終在主執行緒上執行工作的dispath queue。也可以建立自己的dispath queue,而且可以建立任意多個。GCD的dispath queue嚴格遵循FIFO(先進先出)原則,新增到dispath queue的工作單元將始終按照加入dispath queue的順序啟動。

3.dispatch queue按先進先出的順序,序列或併發地執行任務

1> serial dispatch queue一次只能執行一個任務, 當前任務完成才開始出列並啟動下一個任務

2> concurrent dispatch queue則儘可能多地啟動任務併發執行

三、建立和管理dispatch queue

1.獲得全域性併發Dispatch Queue (concurrent dispatch queue)

1> 併發dispatch queue可以同時並行地執行多個任務,不過併發queue仍然按先進先出的順序來啟動任務。併發queue會在之前的任務完成之前就出列下一個任務並開始執行。併發queue同時執行的任務數量會根據應用和系統動態變化,各種因素包括:可用核數量、其它程序正在執行的工作數量、其它序列dispatch queue中優先任務的數量等.

2> 系統給每個應用提供三個併發dispatch queue,整個應用內全域性共享,三個queue的區別是優先順序。你不需要顯式地建立這些queue,使用dispatch_get_global_queue函式來獲取這三個queue:

  1. // 獲取預設優先順序的全域性併發dispatch queue
  2. dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
第一個引數用於指定優先順序,分別使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW兩個常來獲取高和低優先順序的兩個queue;第二個引數目前未使用到,預設0即可

3> 雖然dispatch queue是引用計數的物件,但你不需要retain和release全域性併發queue。因為這些queue對應用是全域性的,retain和release呼叫會被忽略。你也不需要儲存這三個queue的引用,每次都直接呼叫dispatch_get_global_queue獲得queue就行了。

2.建立序列Dispatch Queue (serial dispatch queue)

1> 應用的任務需要按特定順序執行時,就需要使用序列Dispatch Queue,序列queue每次只能執行一個任務。你可以使用序列queue來替代鎖,保護共享資源 或可變的資料結構。和鎖不一樣的是,序列queue確保任務按可預測的順序執行。而且只要你非同步地提交任務到序列queue,就永遠不會產生死鎖

2> 你必須顯式地建立和管理所有你使用的序列queue,應用可以建立任意數量的序列queue,但不要為了同時執行更多工而建立更多的序列queue。如果你需要併發地執行大量任務,應該把任務提交到全域性併發queue

3> 利用dispatch_queue_create函式建立序列queue,兩個引數分別是queue名和一組queue屬性

  1. dispatch_queue_t queue;  
  2. queue = dispatch_queue_create("cn.itcast.queue", NULL);  

3.執行時獲得公共Queue
GCD提供了函式讓應用訪問幾個公共dispatch queue:

1> 使用dispatch_get_current_queue函式作為除錯用途,或者測試當前queue的標識。在block物件中呼叫這個函式會返回block提交到的queue(這個時候queue應該正在執行中)。在block物件之外呼叫這個函式會返回應用的預設併發queue。
2> 使用dispatch_get_main_queue函式獲得應用主執行緒關聯的序列dispatch queue
3> 使用dispatch_get_global_queue來獲得共享的併發queue

4.Dispatch Queue的記憶體管理

1> Dispatch Queue和其它dispatch物件(還有dispatch source)都是引用計數的資料型別。當你建立一個序列dispatch queue時,初始引用計數為 1,你可以使用dispatch_retain和dispatch_release函式來增加和減少引用計數。當引用計數到達 0 時,系統會非同步地銷燬這個queue

2> 對dispatch物件(如dispatch queue)retain和release 是很重要的,確保它們被使用時能夠保留在記憶體中。和OC物件一樣,通用的規則是如果使用一個傳遞過來的queue,你應該在使用前retain,使用完之後release

3> 你不需要retain或release全域性dispatch queue,包括全域性併發dispatch queue和main dispatch queue

4> 即使你實現的是自動垃圾收集的應用,也需要retain和release建立的dispatch queue和其它dispatch物件。GCD 不支援垃圾收集模型來回收記憶體

四、新增任務到queue

要執行一個任務,你需要將它新增到一個適當的dispatch queue,你可以單個或按組來新增,也可以同步或非同步地執行一個任務,也。一旦進入到queue,queue會負責儘快地執行你的任務。一般可以用一個block來封裝任務內容。

1.新增單個任務到queue

1> 非同步新增任務

你可以非同步或同步地新增一個任務到Queue,儘可能地使用dispatch_async或dispatch_async_f函式非同步地排程任務。因為新增任務到Queue中時,無法確定這些程式碼什麼時候能夠執行。因此非同步地新增block或函式,可以讓你立即排程這些程式碼的執行,然後呼叫執行緒可以繼續去做其它事情。特別是應用主執行緒一定要非同步地 dispatch 任務,這樣才能及時地響應使用者事件

2> 同步新增任務

少數時候你可能希望同步地排程任務,以避免競爭條件或其它同步錯誤。 使用dispatch_sync和dispatch_sync_f函式同步地新增任務到Queue,這兩個函式會阻塞當前呼叫執行緒,直到相應任務完成執行。注意:絕對不要在任務中呼叫 dispatch_sync或dispatch_sync_f函式,並同步排程新任務到當前正在執行的 queue。對於序列queue這一點特別重要,因為這樣做肯定會導致死鎖;而併發queue也應該避免這樣做。

3> 程式碼演示

  1. // 呼叫前,檢視下當前執行緒
  2. NSLog(@"當前呼叫執行緒:%@", [NSThread currentThread]);  
  3. // 建立一個序列queue
  4. dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL);  
  5. dispatch_async(queue, ^{  
  6.     NSLog(@"開啟了一個非同步任務,當前執行緒:%@", [NSThread currentThread]);  
  7. });  
  8. dispatch_sync(queue, ^{  
  9.     NSLog(@"開啟了一個同步任務,當前執行緒:%@", [NSThread currentThread]);  
  10. });  
  11. // 銷燬佇列
  12. dispatch_release(queue);  
列印資訊:
  1. 2013-02-0309:03:37.348 thread[6491:c07] 當前呼叫執行緒:<NSThread: 0x714fa80>{name = (null), num = 1}  
  2. 2013-02-0309:03:37.349 thread[6491:1e03] 開啟了一個非同步任務,當前執行緒:<NSThread: 0x74520a0>{name = (null), num = 3}  
  3. 2013-02-0309:03:37.350 thread[6491:c07] 開啟了一個同步任務,當前執行緒:<NSThread: 0x714fa80>{name = (null), num = 1}  

2.併發地執行迴圈迭代

如果你使用迴圈執行固定次數的迭代, 併發dispatch queue可能會提高效能。

例如下面的for迴圈:

  1. int i;  
  2. int count = 10;  
  3. for (i = 0; i < count; i++) {  
  4.    printf("%d  ",i);  
  5. }  

1> 如果每次迭代執行的任務與其它迭代獨立無關,而且迴圈迭代執行順序也無關緊要的話,你可以呼叫dispatch_apply或dispatch_apply_f函式來替換迴圈。這兩個函式為每次迴圈迭代將指定的block或函式提交到queue。當dispatch到併發 queue時,就有可能同時執行多個迴圈迭代。用dispatch_apply或dispatch_apply_f時你可以指定序列或併發 queue。併發queue允許同時執行多個迴圈迭代,而序列queue就沒太大必要使用了。

下面程式碼使用dispatch_apply替換了for迴圈,你傳遞的block必須包含一個size_t型別的引數,用來標識當前迴圈迭代。第一次迭代這個引數值為0,最後一次值為count - 1

  1. // 獲得全域性併發queue
  2. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  3. size_t count = 10;  
  4. dispatch_apply(count, queue, ^(size_t i) {  
  5.     printf("%zd ", i);  
  6. });  
  7. // 銷燬佇列
  8. dispatch_release(queue);  
列印資訊:
  1. 1203456789
可以看出,這些迭代是併發執行的

和普通for迴圈一樣,dispatch_apply和dispatch_apply_f函式也是在所有迭代完成之後才會返回,因此這兩個函式會阻塞當前執行緒,主執行緒中呼叫這兩個函式必須小心,可能會阻止事件處理迴圈並無法響應使用者事件。所以如果迴圈程式碼需要一定的時間執行,可以考慮在另一個執行緒中呼叫這兩個函式。如果你傳遞的引數是序列queue,而且正是執行當前程式碼的queue,就會產生死鎖

3.在主執行緒中執行任務

1> GCD提供一個特殊的dispatch queue,可以在應用的主執行緒中執行任務。只要應用主執行緒設定了run loop(由CFRunLoopRef型別或NSRunLoop物件管理),就會自動建立這個queue,並且最後會自動銷燬。非Cocoa應用如果不顯式地設定run loop, 就必須顯式地呼叫dispatch_main函式來顯式地啟用這個dispatch queue,否則雖然你可以新增任務到queue,但任務永遠不會被執行。

2> 呼叫dispatch_get_main_queue函式獲得應用主執行緒的dispatch queue,新增到這個queue的任務由主執行緒序列化執行

3> 程式碼實現,比如非同步下載圖片後,回到主執行緒顯示圖片

  1. // 非同步下載圖片
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  3.     NSURL *url = [NSURL URLWithString:@"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"];  
  4.     UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];  
  5.     // 回到主執行緒顯示圖片
  6.     dispatch_async(dispatch_get_main_queue(), ^{  
  7.         self.imageView.image = image;  
  8.     });  
  9. });  

4.任務中使用Objective-C物件

GCD支援Cocoa記憶體管理機制,因此可以在提交到queue的block中自由地使用Objective-C物件。每個dispatch queue維護自己的autorelease pool確保釋放autorelease物件,但是queue不保證這些物件實際釋放的時間。如果應用消耗大量記憶體,並且建立大量autorelease物件,你需要建立自己的autorelease pool,用來及時地釋放不再使用的物件。

五、暫停和繼續queue

我們可以使用dispatch_suspend函式暫停一個queue以阻止它執行block物件;使用dispatch_resume函式繼續dispatch queue。呼叫dispatch_suspend會增加queue的引用計數,呼叫dispatch_resume則減少queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。因此你必須對應地呼叫suspend和resume函式。掛起和繼續是非同步的,而且只在執行block之間(比如在執行一個新的block之前或之後)生效。掛起一個queue不會導致正在執行的block停止。

六、Dispatch Group的使用

假設有這樣一個需求:從網路上下載兩張不同的圖片,然後顯示到不同的UIImageView上去,一般可以這樣實現

  1. // 根據url獲取UIImage
  2. - (UIImage *)imageWithURLString:(NSString *)urlString {  
  3.     NSURL *url = [NSURL URLWithString:urlString];  
  4.     NSData *data = [NSData dataWithContentsOfURL:url];  
  5.     return [UIImage imageWithData:data];  
  6. }  
  7. - (void)downloadImages {  
  8.     // 非同步下載圖片
  9.     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  10.         // 下載第一張圖片
  11.         NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
  12.         UIImage *image1 = [self imageWithURLString:url1];  
  13.         // 下載第二張圖片
  14.         NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
  15.         UIImage *image2 = [self imageWithURLString:url2];  
  16.         // 回到主執行緒顯示圖片
  17.         dispatch_async(dispatch_get_main_queue(), ^{  
  18.             self.imageView1.image = image1;  
  19.             self.imageView2.image = image2;  
  20.         });  
  21.     });  
  22. }  
雖然這種方案可以解決問題,但其實兩張圖片的下載過程並不需要按順序執行,併發執行它們可以提高執行速度。有個注意點就是必須等兩張圖片都下載完畢後才能回到主執行緒顯示圖片。Dispatch Group能夠在這種情況下幫我們提升效能。下面先看看Dispatch Group的用處:

我們可以使用dispatch_group_async函式將多個任務關聯到一個Dispatch Group和相應的queue中,group會併發地同時執行這些任務。而且Dispatch Group可以用來阻塞一個執行緒, 直到group關聯的所有的任務完成執行。有時候你必須等待任務完成的結果,然後才能繼續後面的處理。

下面用Dispatch Group優化上面的程式碼:

  1. // 根據url獲取UIImage
  2. - (UIImage *)imageWithURLString:(NSString *)urlString {  
  3.     NSURL *url = [NSURL URLWithString:urlString];  
  4.     NSData *data = [NSData dataWithContentsOfURL:url];  
  5.     // 這裡並沒有自動釋放UIImage物件
  6.     return [[UIImage alloc] initWithData:data];  
  7. }  
  8. - (void)downloadImages {  
  9.     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  10.     // 非同步下載圖片
  11.     dispatch_async(queue, ^{  
  12.         // 建立一個組
  13.         dispatch_group_t group = dispatch_group_create();  
  14.         __block UIImage *image1 = nil;  
  15.         __block UIImage *image2 = nil;  
  16.         // 關聯一個任務到group
  17.         dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  18.             // 下載第一張圖片
  19.             NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
  20.             image1 = [self imageWithURLString:url1];  
  21.         });  
  22.         // 關聯一個任務到group
  23.         dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  24.             // 下載第一張圖片
  25.             NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
  26.             image2 = [self imageWithURLString:url2];  
  27.         });  
  28.         // 等待組中的任務執行完畢,回到主執行緒執行block回撥
  29.         dispatch_group_notify(group, dispatch_get_main_queue(), ^{  
  30.             self.imageView1.image = image1;  
  31.             self.imageView2.image = image2;  
  32.             // 千萬不要在非同步執行緒中自動釋放UIImage,因為當非同步執行緒結束,非同步執行緒的自動釋放池也會被銷燬,那麼UIImage也會被銷燬
  33.             // 在這裡釋放圖片資源
  34.             [image1 release];  
  35.             [image2 release];  
  36.         });  
  37.         // 釋放group
  38.         dispatch_release(group);  
  39.     });  
  40. }  
dispatch_group_notify函式用來指定一個額外的block,該block將在group中所有任務完成後執行