iOS多線程編程的幾種方式

1、線程概述html

    有些程序是一條直線,起點到終點——如簡單的hello world,運行打印完,它的生命週期便結束了,像曇花一現。程序員

    有些程序是一個圓,直到循環將它切斷——像操做系統,一直運行,直到你關機。數據庫

    一個運行着的程序就是一個進程或者叫作一個任務,一個進程至少包含一個線程,線程就是程序的執行流。編程

    Mac和IOS中的程序啓動,建立好一個進程的同時,一個線程便開始運做,這個線程叫作主線程。主線程在程序中的位置和其餘線程不一樣,它是其餘線程最終的父線程,且全部的界面的顯示操做即AppKit或UIKit的操做必須在主線程進行。數組

    系統中每個進程都有本身獨立的虛擬內存空間,而同一個進程中的多個線程則公用進程的內存空間。xcode

    每建立一個新的進程,都須要一些內存(如每一個線程有本身的stack空間)和消耗必定的CPU時間。安全

    當多個進程對同一個資源出現爭奪的時候須要注意線程安全問題。網絡

    建立線程:建立一個新的線程就是給進程增長一個執行流,因此新建一個線程須要提供一個函數或者方法做爲線程的進口。多線程

    概要提示:併發

    iPhone中的線程應用並非無節制的,官方給出的資料顯示,iPhone OS下的主線程的堆棧大小是1M,第二個線程開始就是512KB,而且該值不能經過編譯器開關或線程API函數來更改,只有主線程有直接修改UI的能力。

2、簡介

    iOS有三種多線程編程的技術,分別是:

  (一)NSThread

  (二)Cocoa NSOperation

  (三)GCD(全稱:Grand Central Dispatch)
 
    這三種編程方式從上到下,抽象度層次是從低到高的,抽象度越高的使用越簡單,也是Apple最推薦使用的。
 
3、三種方式的優缺點
 
    1) NSThread:
    優勢:NSThread 比其餘兩個輕量級
    缺點:須要本身管理線程的生命週期,線程同步。線程同步對數據的加鎖會有必定的系統開銷。
 
    NSThread實現的技術有下面三種:
    
    通常使用cocoa thread 技術。
 
    2)Cocoa NSOperation
    優勢:不須要關心線程管理,數據同步的事情,能夠把精力放在本身須要執行的操做上。
    Cocoa operation 相關的類是 NSOperation ,NSOperationQueue。
    NSOperation是個抽象類,使用它必須用它的子類,能夠實現它或者使用它定義好的兩個子類:NSInvocationOperation 和 NSBlockOperation。
    建立NSOperation子類的對象,把對象添加到NSOperationQueue隊列裏執行。
 
    3)GCD
    Grand Central Dispatch (GCD)是Apple開發的一個多核編程的解決方法。在iOS4.0開始以後才能使用。
    GCD是一個替代諸如NSThread, NSOperationQueue, NSInvocationOperation等技術的很高效和強大的技術。如今的iOS系統都升級到7了,因此不用擔憂該技術不能使用。
 
    線程之間的通信
    利用NSObject的一些類方法就能夠作到。
    在應用程序主線程中作事情:
1 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
2 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

    在指定線程中作事情:

1 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
2 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

    在當前線程中作事情:

1 - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
2 - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)array;

    取消發送給當前線程的某個消息:

 1 cancelPreviousPerformRequestsWithTarget: 2 cancelPreviousPerformRequestsWithTarget:selector:object: 

 

4、三種編程技術的使用
 
(一)NSThread的使用
 NSThread有兩種建立方式:
1 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 
2 + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument 

第一個是實例方法,第二個是類方法。使用方式以下:

1 1、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];   
2   
3 2、NSThread* myThread = [[NSThread alloc] initWithTarget:self   
4                                         selector:@selector(doSomething:)   
5                                         object:nil];   
6 [myThread start];
參數的意義:
selector:線程執行的方法,這個selector只能有一個參數,並且不能有返回值。
target:selector消息發送的對象
object:傳輸給target的惟一參數,也能夠是nil
 
第一種方式會直接建立線程而且開始運行線程,第二種方式是先建立線程對象,而後再運行線程操做,在運行線程操做前能夠設置線程的優先級等線程信息。
 
不顯式建立線程的方法:
  1 [Obj performSelectorInBackground:@selector(doSomething) withObject:nil];  
 
下載圖片的例子:
新建SingleViewApp項目,並在xib文件上放置一個imageView控件。按住control鍵拖到viewController.h文件中建立imageView IBOutlet ViewController.m中實現: 
 1 //   
 2 //  ViewController.m   
 3 //  NSThreadDemo   
 4 //   
 5 //  Created by rongfzh on 12-9-23.   
 6 //  Copyright (c) 2012年 rongfzh. All rights reserved.   
 7 //   
 8    
 9 #import "ViewController.h"   
10 #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"   
11 @interface ViewController ()   
12 
13 @end
14 
15 @implementation ViewController 
16 
17 -(void)downloadImage:(NSString *) url{   
18     NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];   
19     UIImage *image = [[UIImage alloc]initWithData:data];   
20     if(image == nil){   
21            
22     }else{   
23         [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];   
24     }   
25 } 
26 
27 -(void)updateUI:(UIImage*) image{   
28     self.imageView.image = image;   
29 }
30 
31 - (void)viewDidLoad   
32 {   
33     [super viewDidLoad];
34 
35 //    [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL];   
36     NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL];   
37     [thread start];   
38 }   
39 
40 - (void)didReceiveMemoryWarning   
41 {   
42     [super didReceiveMemoryWarning];   
43     // Dispose of any resources that can be recreated.   
44 }  
45 
46 @end
47    
Code

 

線程間通信
線程下載完圖片後怎麼通知主線程更新界面呢?
  1 [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];  
 
performSelectorOnMainThread是NSObject的方法,除了能夠更新主線程的數據外,還能夠更新其餘線程的好比:
  1 performSelector:onThread:withObject:waitUntilDone: 
 
運行下載圖片:
 
線程同步
咱們演示一個經典的賣票的例子來說NSThread的線程同步: 
 1 #import <UIKit/UIKit.h> 
 2 
 3 @class ViewController; 
 4 
 5 @interface AppDelegate : UIResponder <UIApplicationDelegate>   
 6 {   
 7     int tickets;   
 8     int count;   
 9     NSThread* ticketsThreadone;   
10     NSThread* ticketsThreadtwo;   
11     NSCondition* ticketsCondition;   
12     NSLock *theLock;   
13 }   
14 @property (strong, nonatomic) UIWindow *window;   
15    
16 @property (strong, nonatomic) ViewController *viewController;   
17    
18 @end   

 

 1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions   
 2 {   
 3        
 4     tickets = 100;   
 5     count = 0;   
 6     theLock = [[NSLock alloc] init];   
 7     // 鎖對象   
 8     ticketsCondition = [[NSCondition alloc] init];   
 9     ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
10     [ticketsThreadone setName:@"Thread-1"];   
11     [ticketsThreadone start];
12 
13     ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
14     [ticketsThreadtwo setName:@"Thread-2"];   
15     [ticketsThreadtwo start]; 
16 
17     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];   
18     // Override point for customization after application launch.   
19     self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];   
20     self.window.rootViewController = self.viewController;   
21     [self.window makeKeyAndVisible];   
22     return YES;   
23 }   
24 
25 - (void)run{   
26     while (TRUE) {   
27         // 上鎖   
28 //        [ticketsCondition lock];   
29         [theLock lock];   
30         if(tickets >= 0){   
31             [NSThread sleepForTimeInterval:0.09];   
32             count = 100 - tickets;   
33             NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]);   
34             tickets--;   
35         }else{   
36             break;   
37         }   
38         [theLock unlock];   
39 //        [ticketsCondition unlock];   
40     }   
41 }   

 若是沒有線程同步的lock,賣票數多是-1.加上lock加上lock以後線程同步保證了數據的正確性。

上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經註釋了。

線程的順序執行
他們均可以經過[ticketsCondition signal]; 發送信號的方式,在一個線程喚醒另一個線程的等待。好比:
 1 #import "AppDelegate.h"   
 2    
 3 #import "ViewController.h"   
 4    
 5 @implementation AppDelegate   
 6    
 7 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions   
 8 {  
 9     tickets = 100;   
10     count = 0;   
11     theLock = [[NSLock alloc] init];   
12     // 鎖對象 
13     ticketsCondition = [[NSCondition alloc] init];   
14     ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
15     [ticketsThreadone setName:@"Thread-1"];   
16     [ticketsThreadone start];
17 
18     ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
19     [ticketsThreadtwo setName:@"Thread-2"];   
20     [ticketsThreadtwo start];
21 
22     NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];   
23     [ticketsThreadthree setName:@"Thread-3"];   
24     [ticketsThreadthree start];       
25     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];   
26     // Override point for customization after application launch.   
27     self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];   
28     self.window.rootViewController = self.viewController;   
29     [self.window makeKeyAndVisible];   
30     return YES;
31 }
32 
33 -(void)run3{   
34     while (YES) {   
35         [ticketsCondition lock];   
36         [NSThread sleepForTimeInterval:3];   
37         [ticketsCondition signal];   
38         [ticketsCondition unlock];   
39     }   
40 }   
41 
42 - (void)run{   
43     while (TRUE) {   
44         // 上鎖   
45         [ticketsCondition lock];   
46         [ticketsCondition wait];   
47         [theLock lock];   
48         if(tickets >= 0){   
49             [NSThread sleepForTimeInterval:0.09];   
50             count = 100 - tickets;   
51             NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]);   
52             tickets--;   
53         }else{   
54             break;   
55         }   
56         [theLock unlock];   
57         [ticketsCondition unlock];   
58     }   
59 }   
View Code
wait是等待,我加了一個 線程3 去喚醒其餘兩個線程鎖中的wait
 
其餘同步
咱們可使用指令 @synchronized 來簡化 NSLock的使用,這樣咱們就沒必要顯示編寫建立NSLock,加鎖並解鎖相關代碼。
1 - (void)doSomeThing:(id)anObj 
2 { 
3     @synchronized(anObj) 
4     { 
5         // Everything between the braces is protected by the @synchronized directive. 
6     } 
7 } 

還有其餘的一些鎖對象,好比:循環鎖NSRecursiveLock,條件鎖NSConditionLock,分佈式鎖NSDistributedLock等等,能夠本身看官方文檔學習。

NSThread下載圖片的例子代碼:http://download.csdn.net/detail/totogo2010/4591149

 

(二)Cocoa Operation的使用

NSOperation實例封裝了須要執行的操做和執行操做所需的數據,而且可以以併發或非併發的方式執行這個操做。NSOperation自己是抽象基類,所以必須使用它的子類,使用NSOperation子類的方式有2種:

1> Foundation框架提供了兩個具體子類直接供咱們使用:NSInvocationOperation和NSBlockOperation

2> 自定義子類繼承NSOperation,實現內部相應的方法

 

執行操做:

NSOperation調用start方法便可開始執行操做,NSOperation對象默認按同步方式執行,也就是在調用start方法的那個線程中直接執行。NSOperation對象的isConcurrent方法會告訴咱們這個操做相對於調用start方法的線程,是同步仍是異步執行。isConcurrent方法默認返回NO,表示操做與調用線程同步執行。

 

取消操做:

operation開始執行以後, 默認會一直執行操做直到完成,咱們也能夠調用cancel方法中途取消操做。

 1 [operation cancel];  

 

監聽操做的執行:

若是咱們想在一個NSOperation執行完畢後作一些事情,就調用NSOperation的setCompletionBlock方法來設置想作的事情。

1 operation.completionBlock = ^() {  
2     NSLog(@"執行完畢");  
3 };
4 
5 或者
6 
7 [operation setCompletionBlock:^() {  
8     NSLog(@"執行完畢");  
9 }]; 

 

1)NSInvocationOperation

基於一個對象和selector來建立操做。若是你已經有現有的方法來執行須要的任務,就可使用這個類。

建立並執行操做:

1 // 這個操做是:調用self的run方法
2 NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
3 // 開始執行任務(同步執行)
4 [operation start];
 
例子:
這裏一樣,咱們實現一個下載圖片的例子。新建一個Single View app,拖放一個ImageView控件到xib界面。
實現代碼以下:
 1 #import "ViewController.h"   
 2 #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"   
 3    
 4 @interface ViewController ()   
 5    
 6 @end 
 7 
 8 @implementation ViewController   
 9    
10 - (void)viewDidLoad   
11 {   
12     [super viewDidLoad];   
13     NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self   
14                                                                            selector:@selector(downloadImage:)   
15                                                                              object:kURL];   
16        
17     NSOperationQueue *queue = [[NSOperationQueue alloc]init];   
18     [queue addOperation:operation];   
19     // Do any additional setup after loading the view, typically from a nib.   
20 } 
21 
22 -(void)downloadImage:(NSString *)url{   
23     NSLog(@"url:%@", url);   
24     NSURL *nsUrl = [NSURL URLWithString:url];   
25     NSData *data = [[NSData alloc]initWithContentsOfURL:nsUrl];   
26     UIImage * image = [[UIImage alloc]initWithData:data];   
27     [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];   
28 }   
29 
30 -(void)updateUI:(UIImage*) image{   
31     self.imageView.image = image;   
32 } 
代碼註釋:
1.viewDidLoad方法裏能夠看到咱們用NSInvocationOperation建了一個後臺線程,而且放到NSOperationQueue中。後臺線程執行downloadImage方法。
2.downloadImage 方法處理下載圖片的邏輯。下載完成後用performSelectorOnMainThread執行主線程updateUI方法。updateUI 並把下載的圖片顯示到圖片控件中。

運行能夠看到下載圖片顯示在界面上。

 

2)NSBlockOperation

可以併發地執行一個或多個block對象,全部相關的block都執行完以後,操做纔算完成。

建立並執行操做:

1 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){
2         NSLog(@"執行了一個新的操做,線程:%@", [NSThread currentThread]);
3 }];
4  // 開始執行任務(這裏仍是同步執行)
5 [operation start];

經過addExecutionBlock方法添加block操做:

 1 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){
 2 &nbsp;&nbsp;&nbsp; NSLog(@"執行第1次操做,線程:%@", [NSThread currentThread]);
 3 }];
 4 
 5 [operation addExecutionBlock:^() {
 6 &nbsp;&nbsp;&nbsp; NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]);
 7 }];
 8 
 9 [operation addExecutionBlock:^() {
10 &nbsp;&nbsp;&nbsp; NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]);
11 }];
12 
13 [operation addExecutionBlock:^() {
14 &nbsp;&nbsp;&nbsp; NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]);
15 }];
16 
17 // 開始執行任務
18 [operation start];
打印信息以下:
1 2013-02-02 21:38:46.102 thread[4602:c07] 又執行了1個新的操做,線程:<NSThread: 0x7121d50>{name = (null), num = 1}
2 2013-02-02 21:38:46.102 thread[4602:3f03] 又執行了1個新的操做,線程:<NSThread: 0x742e1d0>{name = (null), num = 5}
3 2013-02-02 21:38:46.102 thread[4602:1b03] 執行第1次操做,線程:<NSThread: 0x742de50>{name = (null), num = 3}
4 2013-02-02 21:38:46.102 thread[4602:1303] 又執行了1個新的操做,線程:<NSThread: 0x7157bf0>{name = (null), num = 4}

能夠看出,這4個block是併發執行的,也就是在不一樣線程中執行的,num屬性能夠當作是線程的id。

 

3)自定義NSOperation

若是NSInvocationOperation和NSBlockOperation對象不能知足需求, 你能夠直接繼承NSOperation, 並添加任何你想要的行爲。繼承所需的工做量主要取決於你要實現非併發仍是併發的NSOperation。定義非併發的NSOperation要簡單許多,只須要重載-(void)main這個方法,在這個方法裏面執行主任務,並正確地響應取消事件; 對於併發NSOperation, 你必須重寫NSOperation的多個基本方法進行實現(這裏暫時先介紹非併發的NSOperation)。

非併發的NSOperation:

好比叫作DownloadOperation,用來下載圖片。

1> 繼承NSOperation,重寫main方法,執行主任務

DownloadOperation.h

 1 #import <Foundation/Foundation.h>
 2 @protocol DownloadOperationDelegate;
 3 
 4 @interface DownloadOperation : NSOperation
 5 // 圖片的url路徑
 6 @property (nonatomic, copy) NSString *imageUrl;
 7 // 代理
 8 @property (nonatomic, retain) id<DownloadOperationDelegate> delegate;
 9 
10 - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;
11 @end
12 
13 // 圖片下載的協議
14 @protocol DownloadOperationDelegate <NSObject>
15 - (void)downloadFinishWithImage:(UIImage *)image;
16 @end

DownloadOperation.m

 1 #import "DownloadOperation.h"
 2 
 3 @implementation DownloadOperation
 4 @synthesize delegate = _delegate;
 5 @synthesize imageUrl = _imageUrl;
 6 
 7 // 初始化
 8 - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {
 9     if (self = [super init]) {
10         self.imageUrl = url;
11         self.delegate = delegate;
12     }
13     return self;
14 }
15 // 釋放內存
16 - (void)dealloc {
17     [super dealloc];
18     [_delegate release];
19     [_imageUrl release];
20 }
21 
22 // 執行主任務
23 - (void)main {
24     // 新建一個自動釋放池,若是是異步執行操做,那麼將沒法訪問到主線程的自動釋放池
25     @autoreleasepool {
26         // ....
27     }
28 }
29 @end

2> 正確響應取消事件

operation開始執行以後,會一直執行任務直到完成,或者顯式地取消操做。取消可能發生在任什麼時候候,甚至在operation執行以前。儘管NSOperation提供了一個方法,讓應用取消一個操做,可是識別出取消事件則是咱們本身的事情。若是operation直接終止, 可能沒法回收全部已分配的內存或資源。所以operation對象須要檢測取消事件,並優雅地退出執行

NSOperation對象須要按期地調用isCancelled方法檢測操做是否已經被取消,若是返回YES(表示已取消),則當即退出執行。無論是自定義NSOperation子類,仍是使用系統提供的兩個具體子類,都須要支持取消。isCancelled方法自己很是輕量,能夠頻繁地調用而不產生大的性能損失。

如下地方可能須要調用isCancelled:
* 在執行任何實際的工做以前
* 在循環的每次迭代過程當中,若是每一個迭代相對較長可能須要調用屢次
* 代碼中相對比較容易停止操做的任何地方

DownloadOperation的main方法實現以下:

 1 - (void)main {
 2     // 新建一個自動釋放池,若是是異步執行操做,那麼將沒法訪問到主線程的自動釋放池
 3     @autoreleasepool {
 4         if (self.isCancelled) return;
 5         
 6         // 獲取圖片數據
 7         NSURL *url = [NSURL URLWithString:self.imageUrl];
 8         NSData *imageData = [NSData dataWithContentsOfURL:url];
 9         
10         if (self.isCancelled) {
11             url = nil;
12             imageData = nil;
13             return;
14         }
15         
16         // 初始化圖片
17         UIImage *image = [UIImage imageWithData:imageData];
18         
19         if (self.isCancelled) {
20             image = nil;
21             return;
22         }
23         
24         if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {
25             // 把圖片數據傳回到主線程
26             [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];
27         }
28     }
29 }

 

如何控制線程池中的線程數?
隊列裏能夠加入不少個NSOperation, 能夠把NSOperationQueue看做一個線程池,可往線程池中添加操做(NSOperation)到隊列中。線程池中的線程可看做消費者,從隊列中取走操做,並執行它。
 
經過下面的代碼設置:
  1 [queue setMaxConcurrentOperationCount:5]; 
 
線程池中的線程數,也就是併發操做數。默認狀況下是-1,-1表示沒有限制,這樣會同時運行隊列中的所有的操做。
 
(三)GCD的使用
 Grand Central Dispatch 簡稱(GCD)是蘋果公司開發的技術,以優化的應用程序支持多核心處理器和其餘的對稱多處理系統的系統。這創建在任務並行執行的線程池模式的基礎上的。它首次發佈在Mac OS X 10.6 ,iOS 4及以上也可用。
GCD 是 libdispatch 的市場名稱,而 libdispatch 做爲 Apple 的一個庫,爲併發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具備如下優勢:
1.GCD 能經過推遲昂貴計算任務並在後臺運行它們來改善你的應用的響應性能。
2.GCD 提供一個易於使用的併發模型而不只僅只是鎖和線程,以幫助咱們避開併發陷阱。
3.GCD 具備在常見模式(例如單例)上用更高性能的原語優化你的代碼的潛在能力。
 
先回顧幾個線程相關的概念:
Serial vs. Concurrent 串行 vs. 併發
這些術語描述當任務相對於其它任務被執行,任務串行執行就是每次只有一個任務被執行,任務併發執行就是在同一時間能夠有多個任務被執行。 
 
Synchronous vs. Asynchronous 同步 vs. 異步
在 GCD 中,這些術語描述當一個函數相對於另外一個任務完成,此任務是該函數要求 GCD 執行的。一個同步函數只在完成了它預約的任務後才返回。一個異步函數,恰好相反,會當即返回,預約的任務會完成但不會等它完成。所以,一個異步函數不會阻塞當前線程去執行下一個函數。
 
Critical Section 臨界區
每一個進程中訪問臨界資源的那段代碼稱爲臨界區(Critical Section)(臨界資源是一次僅容許一個進程使用的共享資源)。每次只准許一個進程進入臨界區,進入後不容許其餘進程進入。不管是硬件臨界資源,仍是軟件臨界資源,多個進程必須互斥地對它進行訪問。
 
Race Condition 競態條件
當兩個線程競爭同一資源時,若是對資源的訪問順序敏感(即線程訪問資源的順序會致使不一樣的結果),就稱存在競態條件。致使競態條件發生的代碼區稱做臨界區。在臨界區中使用適當的同步就能夠避免競態條件。
 
Deadlock 死鎖
兩個(有時更多)東西——在大多數狀況下,是線程——所謂的死鎖是指它們都卡住了,並等待對方完成或執行其它操做。第一個不能完成是由於它在等待第二個的完成。但第二個也不能完成,由於它在等待第一個的完成。
 
Thread Safe 線程安全
線程安全的代碼能在多線程或併發任務中被安全的調用,而不會致使任何問題(數據損壞,崩潰,等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary 。你能夠在同一時間在多個線程中使用它而不會有問題。另外一方面,NSMutableDictionary 就不是線程安全的,應該保證一次只能有一個線程訪問它。
 
Context Switch 上下文切換
一個上下文切換指當你在單個進程裏切換執行不一樣的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很廣泛,但會帶來一些額外的開銷。
 
Concurrency vs Parallelism 併發與並行

併發行和並行性的區別能夠用饅頭作比喻。前者至關於一我的同時吃三個饅頭和三我的同時吃一個饅頭。

併發性(Concurrence):指兩個或兩個以上的事件或活動在同一時間間隔內發生。併發的實質是物理CPU(也能夠多個物理CPU) 在若干道程序之間多路複用,併發性是對有限物理資源強制行使多用戶共享以提升效率。

並行性(parallelism)指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不一樣CPU上同時執行。

區別:一個處理器同時處理多個任務和多個處理器或者是多核的處理器同時處理多個不一樣的任務。

前者是邏輯上的同時發生(simultaneous),然後者是物理上的同時發生。

二者的聯繫:並行的事件或活動必定是併發的,但反之併發的事件或活動未必是並行的。並行性是併發性的特例,而併發性是並行性的擴展。

 

Queues 隊列
GCD 提供有 dispatch queues 來處理代碼塊,這些隊列管理你提供給 GCD 的任務並用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列裏的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。

全部的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程並行的訪問它們。 GCD 的優勢是顯而易見的,即當你瞭解了調度隊列如何爲你本身代碼的不一樣部分提供線程安全。關於這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工做。

 

Serial Queues 串行隊列

這些任務的執行時機受到 GCD 的控制;惟一能確保的事情是 GCD 一次只執行一個任務,而且按照咱們添加到隊列的順序來執行。

因爲在串行隊列中不會有兩個任務併發運行,所以不會出現同時訪問臨界區的風險;相對於這些任務來講,這就從競態條件下保護了臨界區。因此若是訪問臨界區的惟一方式是經過提交到調度隊列的任務,那麼你就不須要擔憂臨界區的安全問題了。

 

Concurrent Queues 併發隊列

 

注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始後,Block 1等待了好一下子纔開始。一樣, Block 3 在 Block 2 以後纔開始,但它先於 Block 2 完成。

在併發隊列中的任務能獲得的保證是它們會按照被添加的順序開始執行,但這就是所有的保證了。任務可能以任意順序完成,你不會知道什麼時候開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這徹底取決於 GCD 。

什麼時候開始一個 Block 徹底取決於 GCD 。若是一個 Block 的執行時間與另外一個重疊,也是由 GCD 來決定是否將其運行在另外一個不一樣的核心上,若是那個核心可用,不然就用上下文切換的方式來執行不一樣的 Block 。

 

設計:
GCD的工做原理是:讓程序平行排隊的特定任務,根據可用的處理資源,安排他們在任何可用的處理器核心上執行任務。
 
一個任務能夠是一個函數(function)或者是一個block。 GCD的底層依然是用線程實現,不過這樣可讓程序員不用關注實現的細節。
 
GCD中的FIFO隊列稱爲dispatch queue,它能夠保證先進來的任務先獲得執行。
 
dispatch queue分爲下面三種:
Serial:又稱爲private dispatch queues,同時只執行一個任務。Serial queue一般用於同步訪問特定的資源或數據。當你建立多個Serial queue時,雖然它們各自是同步執行的,但Serial queue與Serial queue之間是併發執行的。
Concurrent:又稱爲global dispatch queue,能夠併發地執行多個任務,可是執行完成的順序是隨機的。
Main dispatch queue:它是全局可用的serial queue,它是在應用程序主線程上執行任務的。
 
Queue Types 隊列類型
首先,系統提供給你一個叫作 主隊列(main queue) 的特殊隊列。和其它串行隊列同樣,這個隊列中的任務一次只能執行一個。然而,它能保證全部的任務都在主線程執行,而主線程是惟一可用於更新 UI 的線程。這個隊列就是用於發生消息給 UIView 或發送通知的。
 
系統同時提供給你好幾個併發隊列。它們叫作 全局調度隊列(Global Dispatch Queues) 。目前的四個全局隊列有着不一樣的優先級:background、low、default 以及 high。要知道,Apple 的 API 也會使用這些隊列,因此你添加的任何任務都不會是這些隊列中惟一的任務。
 
最後,你也能夠建立本身的串行隊列或併發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你本身建立的隊列。

 

接下來咱們來了解GCD的使用:

一、經常使用的方法dispatch_async
爲了不界面在處理耗時的操做時卡死,好比讀取網絡數據,IO,數據庫讀寫等,咱們會在另一個線程中處理這些操做,而後通知主線程更新界面。

用GCD實現這個流程的操做比前面介紹的NSThread  NSOperation的方法都要簡單。代碼框架結構以下:

1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
2     // 耗時的操做   
3     dispatch_async(dispatch_get_main_queue(), ^{   
4         // 更新界面   
5     });   
6 });   

若是這樣還不清晰的話,那咱們仍是用上兩篇博客中的下載圖片爲例子,代碼以下:

 1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
 2     NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];   
 3     NSData * data = [[NSData alloc]initWithContentsOfURL:url];   
 4     UIImage *image = [[UIImage alloc]initWithData:data];   
 5     if (data != nil) {   
 6         dispatch_async(dispatch_get_main_queue(), ^{   
 7             self.imageView.image = image;   
 8          });   
 9     }   
10 });  

運行會顯示下載的圖片。

是否是代碼比NSThread 、NSOperation簡潔不少,並且GCD會自動根據任務在多核處理器上分配資源,優化程序。

 

系統給每個應用程序提供了三個concurrent dispatch queues。這三個併發調度隊列是全局的,它們只有優先級的不一樣。由於是全局的,咱們不須要去建立。咱們只須要經過使用函數dispath_get_global_queue去獲得隊列,以下:

 1 dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 

這裏也用到了系統默認就有一個串行隊列main_queue:

 1 dispatch_queue_t mainQ = dispatch_get_main_queue(); 

 

二、dispatch_group_async的使用
dispatch_group_async能夠實現監聽一組任務是否完成,完成後獲得通知執行其餘的操做。這個方法頗有用,好比你執行三個下載任務,當三個任務都下載完成後你才通知界面說完成的了。下面是一段例子代碼:
 1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);   
 2 dispatch_group_t group = dispatch_group_create();   
 3 dispatch_group_async(group, queue, ^{   
 4     [NSThread sleepForTimeInterval:1];   
 5     NSLog(@"group1");   
 6 });   
 7 dispatch_group_async(group, queue, ^{   
 8     [NSThread sleepForTimeInterval:2];   
 9     NSLog(@"group2");   
10 });   
11 dispatch_group_async(group, queue, ^{   
12     [NSThread sleepForTimeInterval:3];   
13     NSLog(@"group3");   
14 });   
15 dispatch_group_notify(group, dispatch_get_main_queue(), ^{   
16     NSLog(@"updateUi");   
17 });   
18 dispatch_release(group);   

 

dispatch_group_async是異步的方法,運行後能夠看到打印結果:

1 2012-09-25 16:04:16.737 gcdTest[43328:11303] group1 
2 2012-09-25 16:04:17.738 gcdTest[43328:12a1b] group2 
3 2012-09-25 16:04:18.738 gcdTest[43328:13003] group3 
4 2012-09-25 16:04:18.739 gcdTest[43328:f803] updateUi 

每隔一秒打印一個,當第三個任務執行後,upadteUi被打印。

 

三、dispatch_barrier_async的使用
dispatch_barrier_async是在前面的任務執行結束後它才執行,並且它後面的任務等它執行完成以後纔會執行
例子代碼以下:
 1 dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT);   
 2 dispatch_async(queue, ^{   
 3     [NSThread sleepForTimeInterval:2];   
 4     NSLog(@"dispatch_async1");   
 5 });   
 6 dispatch_async(queue, ^{   
 7     [NSThread sleepForTimeInterval:4];   
 8     NSLog(@"dispatch_async2");   
 9 });   
10 dispatch_barrier_async(queue, ^{   
11     NSLog(@"dispatch_barrier_async");   
12     [NSThread sleepForTimeInterval:4];   
13    
14 });   
15 dispatch_async(queue, ^{   
16     [NSThread sleepForTimeInterval:1];   
17     NSLog(@"dispatch_async3");   
18 });   

打印結果:

1 2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1 
2 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2 
3 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async 
4 2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3 

請注意執行的時間,能夠看到執行的順序如上所述。

 

四、dispatch_apply 
執行某個代碼片斷N次。
  1 dispatch_apply(5, globalQ, ^(size_t index) { 2 // 執行5次 3 });  
 
五、dispatch_after
 使用dispatch_after延後工做。
 1 - (void)showOrHideNavPrompt 
 2 { 
 3     NSUInteger count = [[PhotoManager sharedManager] photos].count; 
 4     double delayInSeconds = 1.0; 
 5     dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1  
 6     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2  
 7         if (!count) { 
 8             [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; 
 9         } else { 
10             [self.navigationItem setPrompt:nil]; 
11         } 
12     }); 
13 } 
編譯並運行應用。應該有一個輕微地延遲,這有助於抓住用戶的注意力並展現所要作的事情。
 
dispatch_after 工做起來就像一個延遲版的 dispatch_async 。你依然不能控制實際的執行時間,且一旦 dispatch_after 返回也就不能再取消它。主隊列是使用 dispatch_after 的好選擇,在其餘隊列上使用要當心。
 
4、GCD實例
既然本教程的目標是優化且安全的使用 GCD 調用來自不一樣線程的代碼,那麼你將從一個近乎完成的叫作 GooglyPuff 的項目入手。
GooglyPuff 是一個沒有優化,線程不安全的應用,它使用 Core Image 的人臉檢測 API 來覆蓋一對曲棍球眼睛到被檢測到的人臉上。對於基本的圖像,能夠從相機膠捲選擇,或用預設好的URL從互聯網下載。
 
 
完成項目下載以後,將其解壓到某個方便的目錄,再用 Xcode 打開它並編譯運行。這個應用看起來以下圖所示:
注意當你選擇 Le Internet 選項下載圖片時,一個 UIAlertView 過早地彈出。你將在本系列教程地第二部分修復這個問題。
 
這個項目中有四個有趣的類:
1. PhotoCollectionViewController:它是應用開始的第一個視圖控制器。它用縮略圖展現全部選定的照片。
2. PhotoDetailViewController:它執行添加曲棍球眼睛到圖像上的邏輯,並用一個 UIScrollView 來顯示結果圖片。
3. Photo:這是一個類簇,它根據一個 NSURL 的實例或一個 ALAsset 的實例來實例化照片。這個類提供一個圖像、縮略圖以及從 URL 下載的狀態。
4. PhotoManager:它管理全部 Photo 的實例.
 
用 dispatch_async 處理後臺任務
回到應用並從你的相機膠捲添加一些照片或使用 Le Internet 選項下載一些。
 
注意在按下 PhotoCollectionViewController 中的一個 UICollectionViewCell 到生成一個新的 PhotoDetailViewController 之間花了多久時間;你會注意到一個明顯的滯後,特別是在比較慢的設備上查看很大的圖。
 
在重載 UIViewController 的 viewDidLoad 時容易加入太多雜波(too much clutter),這一般會引發視圖控制器出現前更長的等待。若是可能,最好是卸下一些工做放到後臺,若是它們不是絕對必需要運行在加載時間裏。
 
打開 PhotoDetailViewController 並用下面的實現替換 viewDidLoad:
 1 - (void)viewDidLoad 
 2 {    
 3     [super viewDidLoad]; 
 4     NSAssert(_image, @"Image not set; required to use view controller"); 
 5     self.photoImageView.image = _image; 
 6  
 7     //Resize if neccessary to ensure it's not pixelated 
 8     if (_image.size.height <= self.photoImageView.bounds.size.height && 
 9         _image.size.width <= self.photoImageView.bounds.size.width) { 
10         [self.photoImageView setContentMode:UIViewContentModeCenter]; 
11     } 
12  
13     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 
14         UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; 
15         dispatch_async(dispatch_get_main_queue(), ^{ // 2 
16             [self fadeInNewImage:overlayImage]; // 3 
17         }); 
18     }); 
19 } 

下面來講明上面的新代碼所作的事:

1. 你首先將工做從主線程移到全局線程。由於這是一個 dispatch_async() ,Block 會被異步地提交,意味着調用線程地執行將會繼續。這就使得 viewDidLoad 更早地在主線程完成,讓加載過程感受起來更加快速。同時,一我的臉檢測過程會啓動並將在稍後完成。
2. 在這裏,人臉檢測過程完成,並生成了一個新的圖像。既然你要使用此新圖像更新你的 UIImageView ,那麼你就添加一個新的 Block 到主線程。記住——你必須老是在主線程訪問 UIKit 的類。
3. 最後,你用 fadeInNewImage: 更新 UI ,它執行一個淡入過程切換到新的曲棍球眼睛圖像。
編譯並運行你的應用;選擇一個圖像而後你會注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在以後就加上了。這給應用帶來了不錯的效果,和以前的顯示差異巨大。
 
進一步,若是你試着加載一個超大的圖像,應用不會在加載視圖控制器上「掛住」,這就使得應用具備很好伸縮性。
 
正如以前提到的, dispatch_async 添加一個 Block 都隊列就當即返回了。任務會在以後由 GCD 決定執行。當你須要在後臺執行一個基於網絡或 CPU 緊張的任務時就使用 dispatch_async ,這樣就不會阻塞當前線程。
 
下面是一個關於在 dispatch_async 上如何以及什麼時候使用不一樣的隊列類型的快速指導:
1. 自定義串行隊列:當你想串行執行後臺任務並追蹤它時就是一個好選擇。這消除了資源爭用,由於你知道一次只有一個任務在執行。注意若你須要來自某個方法的數據,你必須內聯另外一個 Block 來找回它或考慮使用 dispatch_sync。
2. 主隊列(串行):這是在一個併發隊列上完成任務後更新 UI 的共同選擇。要這樣作,你將在一個 Block 內部編寫另外一個 Block 。以及,若是你在主隊列調用 dispatch_async 到主隊列,你能確保這個新任務將在當前方法完成後的某個時間執行。
3. 併發隊列:這是在後臺執行非 UI 工做的共同選擇。
 
使用 dispatch_after 延後工做
稍微考慮一下應用的 UX 。是否用戶第一次打開應用時會困惑於不知道作什麼?你是這樣嗎? :]
 
若是用戶的 PhotoManager 裏尚未任何照片,那麼顯示一個提示會是個好主意!然而,你一樣要考慮用戶的眼睛會如何在主屏幕上瀏覽:若是你太快的顯示一個提示,他們的眼睛還徘徊在視圖的其它部分上,他們極可能會錯過它。
 
顯示提示以前延遲一秒鐘就足夠捕捉到用戶的注意,他們此時已經第一次看過了應用。
 
添加以下代碼到到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的廢止實現裏:
 1 - (void)showOrHideNavPrompt 
 2 { 
 3     NSUInteger count = [[PhotoManager sharedManager] photos].count; 
 4     double delayInSeconds = 1.0; 
 5     dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1  
 6     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2  
 7         if (!count) { 
 8             [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; 
 9         } else { 
10             [self.navigationItem setPrompt:nil]; 
11         } 
12     }); 
13 } 
showOrHideNavPrompt 在 viewDidLoad 中執行,以及 UICollectionView 被從新加載的任什麼時候候。按照註釋數字順序看看:
1. 你聲明瞭一個變量指定要延遲的時長。
2. 而後等待 delayInSeconds 給定的時長,再異步地添加一個 Block 到主線程。
 
編譯並運行應用。應該有一個輕微地延遲,這有助於抓住用戶的注意力並展現所要作的事情。
 
dispatch_after 工做起來就像一個延遲版的 dispatch_async 。你依然不能控制實際的執行時間,且一旦 dispatch_after 返回也就不能再取消它。
 
不知道什麼時候適合使用 dispatch_after ?
1. 自定義串行隊列:在一個自定義串行隊列上使用 dispatch_after 要當心。你最好堅持使用主隊列。
2. 主隊列(串行):是使用 dispatch_after 的好選擇;Xcode 提供了一個不錯的自動完成模版。
3. 併發隊列:在併發隊列上使用 dispatch_after 也要當心;你會這樣作就比較罕見。仍是在主隊列作這些操做吧。
 
讓你的單例線程安全
單例,不論喜歡仍是討厭,它們在 iOS 上的流行狀況就像網上的貓。 :]
 
一個常見的擔心是它們經常不是線程安全的。這個擔心十分合理,基於它們的用途:單例經常被多個控制器同時訪問。
 
單例的線程擔心範圍從初始化開始,到信息的讀和寫。PhotoManager 類被實現爲單例——它在目前的狀態下就會被這些問題所困擾。要看看事情如何很快地失去控制,你將在單例實例上建立一個控制好的競態條件。
 
導航到 PhotoManager.m 並找到 sharedManager ;它看起來以下:
1 + (instancetype)sharedManager     
2 { 
3     static PhotoManager *sharedPhotoManager = nil; 
4     if (!sharedPhotoManager) { 
5         sharedPhotoManager = [[PhotoManager alloc] init]; 
6         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
7     } 
8     return sharedPhotoManager; 
9 } 
當前狀態下,代碼至關簡單;你建立了一個單例並初始化一個叫作 photosArray 的 NSMutableArray 屬性。
 
然而,if 條件分支不是 線程安全的;若是你屢次調用這個方法,有一個可能性是在某個線程(就叫它線程A)上進入 if 語句塊並可能在 sharedPhotoManager 被分配內存前發生一個上下文切換。而後另外一個線程(線程B)可能進入 if ,分配單例實例的內存,而後退出。
 
當系統上下文切換回線程A,你會分配另一個單例實例的內存,而後退出。在那個時間點,你有了兩個單例的實例——很明顯這不是你想要的(譯者注:這還能叫單例嗎?)!
 
要強制這個(競態)條件發生,替換 PhotoManager.m 中的 sharedManager 爲下面的實現:
 1 + (instancetype)sharedManager   
 2 { 
 3     static PhotoManager *sharedPhotoManager = nil; 
 4     if (!sharedPhotoManager) { 
 5         [NSThread sleepForTimeInterval:2]; 
 6         sharedPhotoManager = [[PhotoManager alloc] init]; 
 7         NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); 
 8         [NSThread sleepForTimeInterval:2]; 
 9         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
10     } 
11     return sharedPhotoManager; 
12 } 
上面的代碼中你用 NSThread 的 sleepForTimeInterval: 類方法來強制發生一個上下文切換。
 
打開 AppDelegate.m 並添加以下代碼到 application:didFinishLaunchingWithOptions: 的最開始處:
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
2     [PhotoManager sharedManager]; 
3 }); 
4  
5 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
6     [PhotoManager sharedManager]; 
7 });
這裏建立了多個異步併發調用來實例化單例,而後引起上面描述的競態條件。
 
編譯並運行項目;查看控制檯輸出,你會看到多個單例被實例化,以下所示:
注意到這裏有好幾行顯示着不一樣地址的單例實例。這明顯違背了單例的目的,對吧?
 
這個輸出向你展現了臨界區被執行屢次,而它只應該執行一次。如今,當然是你本身強制這樣的情況發生,但你能夠想像一下這個情況會怎樣在無心間發生。
 
注意:基於其它你沒法控制的系統事件,NSLog 的數量有時會顯示多個。線程問題極其難以調試,由於它們每每難以重現。
要糾正這個情況,實例化代碼應該只執行一次,並阻塞其它實例在 if 條件的臨界區運行。這恰好就是 dispatch_once 能作的事。
 
在單例初始化方法中用 dispatch_once 取代 if 條件判斷,以下所示:
 1 + (instancetype)sharedManager 
 2 { 
 3     static PhotoManager *sharedPhotoManager = nil; 
 4     static dispatch_once_t onceToken; 
 5     dispatch_once(&onceToken, ^{ 
 6         [NSThread sleepForTimeInterval:2]; 
 7         sharedPhotoManager = [[PhotoManager alloc] init]; 
 8         NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); 
 9         [NSThread sleepForTimeInterval:2]; 
10         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
11     }); 
12     return sharedPhotoManager; 
13 } 
編譯並運行你的應用;查看控制檯輸出,你會看到有且僅有一個單例的實例——這就是你對單例的指望!:]
 
如今你已經明白了防止競態條件的重要性,從 AppDelegate.m 中移除 dispatch_async 語句,並用下面的實現替換 PhotoManager 單例的初始化:
 1 + (instancetype)sharedManager 
 2 { 
 3     static PhotoManager *sharedPhotoManager = nil; 
 4     static dispatch_once_t onceToken; 
 5     dispatch_once(&onceToken, ^{ 
 6         sharedPhotoManager = [[PhotoManager alloc] init]; 
 7         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
 8     }); 
 9     return sharedPhotoManager; 
10 } 

dispatch_once() 以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once 的代碼)的不一樣的線程會在臨界區已有一個線程的狀況下被阻塞,直到臨界區完成爲止。

須要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類自己線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的狀況。這些須要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。

 

處理讀者與寫者問題
線程安全實例不是處理單例時的惟一問題。若是單例屬性表示一個可變對象,那麼你就須要考慮是否那個對象自身線程安全。
若是問題中的這個對象是一個 Foundation 容器類,那麼答案是——「極可能不安全」!Apple 維護一個有用且有些心寒的列表,衆多的 Foundation 類都不是線程安全的。 NSMutableArray,已用於你的單例,正在那個列表裏休息。
雖然許多線程能夠同時讀取 NSMutableArray 的一個實例而不會產生問題,但當一個線程正在讀取時讓另一個線程修改數組就是不安全的。你的單例在目前的情況下不能預防這種狀況的發生。
 
要分析這個問題,看看 PhotoManager.m 中的 addPhoto:,轉載以下:
1 - (void)addPhoto:(Photo *)photo 
2 { 
3     if (photo) { 
4         [_photosArray addObject:photo]; 
5         dispatch_async(dispatch_get_main_queue(), ^{ 
6             [self postContentAddedNotification]; 
7         }); 
8     } 
9 } 
這是一個寫方法,它修改一個私有可變數組對象。
 
如今看看 photos ,轉載以下:
  1 - (NSArray *)photos 2 { 3 return [NSArray arrayWithArray:_photosArray]; 4 }  
 
這是所謂的讀方法,它讀取可變數組。它爲調用者生成一個不可變的拷貝,防止調用者不當地改變數組,但這不能提供任何保護來對抗當一個線程調用讀方法 photos 的同時另外一個線程調用寫方法 addPhoto: 。
 
這就是軟件開發中經典的讀者寫者問題。GCD 經過用 dispatch barriers 建立一個讀者寫者鎖 提供了一個優雅的解決方案。
 
Dispatch barriers 是一組函數,在併發隊列上工做時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上惟一被執行的條目。這就意味着全部的先於調度障礙提交到隊列的條目必能在這個 Block 執行前完成。
 
當這個 Block 的時機到達,調度障礙執行這個 Block 並確保在那個時間裏隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。
 
下圖顯示了障礙函數對多個異步隊列的影響:
注意到正常部分的操做就如同一個正常的併發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是惟一在執行的事物。在障礙完成後,隊列回到一個正常併發隊列的樣子。
 
下面是你什麼時候會——和不會——使用障礙函數的狀況:
1. 自定義串行隊列:一個很壞的選擇;障礙不會有任何幫助,由於無論怎樣,一個串行隊列一次都只執行一個操做。
2. 全局併發隊列:要當心;這可能不是最好的主意,由於其它系統可能在使用隊列並且你不能壟斷它們只爲你本身的目的。
3. 自定義併發隊列:這對於原子或臨界區代碼來講是極佳的選擇。任何你在設置或實例化的須要線程安全的事物都是使用障礙的最佳候選。
因爲上面惟一像樣的選擇是自定義併發隊列,你將建立一個你本身的隊列去處理你的障礙函數並分開讀和寫函數。且這個併發隊列將容許多個多操做同時進行。
 
打開 PhotoManager.m,添加以下私有屬性到類擴展中:
1 @interface PhotoManager () 
2 @property (nonatomic,strong,readonly) NSMutableArray *photosArray; 
3 @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this 
4 @end 

找到 addPhoto: 並用下面的實現替換它:

 1 - (void)addPhoto:(Photo *)photo 
 2 { 
 3     if (photo) { // 1 
 4         dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2  
 5             [_photosArray addObject:photo]; // 3 
 6             dispatch_async(dispatch_get_main_queue(), ^{ // 4 
 7                 [self postContentAddedNotification];  
 8             }); 
 9         }); 
10     } 
11 }
你新寫的函數是這樣工做的:
1. 在執行下面全部的工做前檢查是否有合法的相片。
2. 添加寫操做到你的自定義隊列。當臨界區在稍後執行時,這將是你隊列中惟一執行的條目。
3. 這是添加對象到數組的實際代碼。因爲它是一個障礙 Block ,這個 Block 永遠不會同時和其它 Block 一塊兒在 concurrentPhotoQueue 中執行。
4. 最後你發送一個通知說明完成了添加圖片。這個通知將在主線程被髮送由於它將會作一些 UI 工做,因此在此爲了通知,你異步地調度另外一個任務到主線程。
這就處理了寫操做,但你還須要實現 photos 讀方法並實例化 concurrentPhotoQueue 。
 
在寫者打擾的狀況下,要確保線程安全,你須要在 concurrentPhotoQueue 隊列上執行讀操做。既然你須要從函數返回,你就不能異步調度到隊列,由於那樣在讀者函數返回以前不必定運行。
 
在這種狀況下,dispatch_sync 就是一個絕好的候選。
 
dispatch_sync() 同步地提交工做並在返回前等待它完成。使用 dispatch_sync 跟蹤你的調度障礙工做,或者當你須要等待操做完成後才能使用 Block 處理過的數據。若是你使用第二種狀況作事,你將不時看到一個 __block 變量寫在 dispatch_sync 範圍以外,以便返回時在 dispatch_sync 使用處理過的對象。
 
但你須要很當心。想像若是你調用 dispatch_sync 並放在你已運行着的當前隊列。這會致使死鎖,由於調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務沒法完成!這將迫使你自覺於你正從哪一個隊列調用——以及你正在傳遞進入哪一個隊列。
 
下面是一個快速總覽,關於在什麼時候以及何處使用 dispatch_sync :
1. 自定義串行隊列:在這個情況下要很是當心!若是你正運行在一個隊列並調用 dispatch_sync 放在同一個隊列,那你就百分百地建立了一個死鎖。
2. 主隊列(串行):同上面的理由同樣,必須很是當心!這個情況一樣有潛在的致使死鎖的狀況。
3. 併發隊列:這纔是作同步工做的好選擇,不管是經過調度障礙,或者須要等待一個任務完成才能執行進一步處理的狀況。
 
繼續在 PhotoManager.m 上工做,用下面的實現替換 photos :
1 - (NSArray *)photos 
2 { 
3     __block NSArray *array; // 1 
4     dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 
5         array = [NSArray arrayWithArray:_photosArray]; // 3 
6     }); 
7     return array; 
8 }
這就是你的讀函數。按順序看看編過號的註釋,有這些:
1. __block 關鍵字容許對象在 Block 內可變。沒有它,array 在 Block 內部就只是只讀的,你的代碼甚至不能經過編譯。
2. 在 concurrentPhotoQueue 上同步調度來執行讀操做。
3. 將相片數組存儲在 array 內並返回它。
 
最後,你須要實例化你的 concurrentPhotoQueue 屬性。修改 sharedManager 以便像下面這樣初始化隊列:
 1 + (instancetype)sharedManager 
 2 { 
 3     static PhotoManager *sharedPhotoManager = nil; 
 4     static dispatch_once_t onceToken; 
 5     dispatch_once(&onceToken, ^{ 
 6         sharedPhotoManager = [[PhotoManager alloc] init]; 
 7         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
 8  
 9         // ADD THIS: 
10         sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", 
11                                                     DISPATCH_QUEUE_CONCURRENT);  
12     }); 
13  
14     return sharedPhotoManager; 
15 } 
這裏使用 dispatch_queue_create 初始化 concurrentPhotoQueue 爲一個併發隊列。第一個參數是反向DNS樣式命名慣例;確保它是描述性的,將有助於調試。第二個參數指定你的隊列是串行仍是併發。
 
注意:當你在網上搜索例子時,你會常常看人們傳遞 0 或者 NULL 給 dispatch_queue_create 的第二個參數。這是一個建立串行隊列的過期方式;明確你的參數老是更好。
恭喜——你的 PhotoManager 單例如今是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。
 
A Visual Review of Queueing 隊列的虛擬回顧
依然沒有 100% 地掌握 GCD 的要領?確保你可使用 GCD 函數輕鬆地建立簡單的例子,使用斷點和 NSLog 語句保證本身明白當下發生的狀況。
 
我在下面提供了兩個 GIF動畫來幫助你鞏固對 dispatch_async 和 dispatch_sync 的理解。包含在每一個 GIF 中的代碼能夠提供視覺輔助;仔細注意 GIF 左邊顯示代碼斷點的每一步,以及右邊相關隊列的狀態。
 
dispatch_sync 回顧
 1 - (void)viewDidLoad 
 2 { 
 3   [super viewDidLoad]; 
 4  
 5   dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
 6  
 7       NSLog(@"First Log"); 
 8  
 9   }); 
10  
11   NSLog(@"Second Log"); 
12 }

下面是圖中幾個步驟的說明:
1. 主隊列一路按順序執行任務——接着是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
2. viewDidLoad 在主線程執行。
3. 主線程目前在 viewDidLoad 內,正要到達 dispatch_sync 。
4. dispatch_sync Block 被添加到一個全局隊列中,將在稍後執行。進程將在主線程掛起直到該 Block 完成。同時,全局隊列併發處理任務;要記得 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。
5. 全局隊列處理 dispatch_sync Block 加入以前已經出如今隊列中的任務。
6. 終於,輪到 dispatch_sync Block 。
7. 這個 Block 完成,所以主線程上的任務能夠恢復。
8. viewDidLoad 方法完成,主隊列繼續處理其餘任務。
 
dispatch_sync 添加任務到一個隊列並等待直到任務完成。dispatch_async 作相似的事情,但不一樣之處是它不會等待任務的完成,而是當即繼續「調用線程」的其它任務。
 
dispatch_async 回顧
 1 - (void)viewDidLoad 
 2 { 
 3   [super viewDidLoad]; 
 4  
 5   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
 6  
 7       NSLog(@"First Log"); 
 8  
 9   }); 
10  
11   NSLog(@"Second Log"); 
12 }

1.主隊列一路按順序執行任務——接着是一個實例化 UIViewController 的任務,其中包含了 viewDidLoad 。
2. viewDidLoad 在主線程執行。
3.主線程目前在 viewDidLoad 內,正要到達 dispatch_async 。
4.dispatch_async Block 被添加到一個全局隊列中,將在稍後執行。
5.viewDidLoad 在添加 dispatch_async 到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列併發地處理它未完成地任務。記住 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。
6.添加到 dispatch_async 的代碼塊開始執行。
7.dispatch_async Block 完成,兩個 NSLog 語句將它們的輸出放在控制檯上。
 
在這個特定的實例中,第二個 NSLog 語句執行,跟着是第一個 NSLog 語句。並不老是這樣——着取決於給定時刻硬件正在作的事情,並且你沒法控制或知曉哪一個語句會先執行。「第一個」 NSLog 在某些調用狀況下會第一個執行。
 
你能夠下載  GooglyPuff 項目,它包含了目前全部本教程中編寫的實現。在本教程的第二部分,你將繼續改進這個項目。
 

糾正過早彈出的提示

你可能已經注意到當你嘗試用 Le Internet 選項來添加圖片時,一個 UIAlertView 會在圖片下載完成以前就彈出,以下如所示:

問題的癥結在 PhotoManagers 的 downloadPhotoWithCompletionBlock: 裏,它目前的實現以下:

 1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 
 2 { 
 3     __block NSError *error; 
 4  
 5     for (NSInteger i = 0; i < 3; i++) { 
 6         NSURL *url; 
 7         switch (i) { 
 8             case 0: 
 9                 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 
10                 break; 
11             case 1: 
12                 url = [NSURL URLWithString:kSuccessKidURLString]; 
13                 break; 
14             case 2: 
15                 url = [NSURL URLWithString:kLotsOfFacesURLString]; 
16                 break; 
17             default: 
18                 break; 
19         } 
20  
21         Photo *photo = [[Photo alloc] initwithURL:url 
22                               withCompletionBlock:^(UIImage *image, NSError *_error) { 
23                                   if (_error) { 
24                                       error = _error; 
25                                   } 
26                               }]; 
27  
28         [[PhotoManager sharedManager] addPhoto:photo]; 
29     } 
30  
31     if (completionBlock) { 
32         completionBlock(error); 
33     } 
34 } 

在方法的最後你調用了 completionBlock ——由於此時你假設全部的照片都已下載完成。但很不幸,此時並不能保證全部的下載都已完成。

 

Photo 類的實例方法用某個 URL 開始下載某個文件並當即返回,但此時下載並未完成。換句話說,當 downloadPhotoWithCompletionBlock: 在其末尾調用 completionBlock 時,它就假設了它本身所使用的方法全都是同步的,並且每一個方法都完成了它們的工做。

 

然而,-[Photo initWithURL:withCompletionBlock:] 是異步執行的,會當即返回——因此這種方式行不通。

 

所以,只有在全部的圖像下載任務都調用了它們本身的 Completion Block 以後,downloadPhotoWithCompletionBlock: 才能調用它本身的 completionBlock 。問題是:你該如何監控併發的異步事件?你不知道它們什麼時候完成,並且它們完成的順序徹底是不肯定的。

或許你能夠寫一些比較 Hacky 的代碼,用多個布爾值來記錄每一個下載的完成狀況,但這樣作就缺失了擴展性,並且說實話,代碼會很難看。

 

幸運的是, 解決這種對多個異步任務的完成進行監控的問題,剛好就是設計 dispatch_group 的目的。

 

Dispatch Groups(調度組)

Dispatch Group 會在整個組的任務都完成時通知你。這些任務能夠是同步的,也能夠是異步的,即使在不一樣的隊列也行。並且在整個組的任務都完成時,Dispatch Group 能夠用同步的或者異步的方式通知你。由於要監控的任務在不一樣隊列,那就用一個 dispatch_group_t 的實例來記下這些不一樣的任務。

 

當組中全部的事件都完成時,GCD 的 API 提供了兩種通知方式。

第一種是 dispatch_group_wait ,它會阻塞當前線程,直到組裏面全部的任務都完成或者等到某個超時發生。這剛好是你目前所須要的。

 

打開 PhotoManager.m,用下列實現替換 downloadPhotosWithCompletionBlock:

 1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 
 2 { 
 3     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 
 4  
 5         __block NSError *error; 
 6         dispatch_group_t downloadGroup = dispatch_group_create(); // 2 
 7  
 8         for (NSInteger i = 0; i < 3; i++) { 
 9             NSURL *url; 
10             switch (i) { 
11                 case 0: 
12                     url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 
13                     break; 
14                 case 1: 
15                     url = [NSURL URLWithString:kSuccessKidURLString]; 
16                     break; 
17                 case 2: 
18                     url = [NSURL URLWithString:kLotsOfFacesURLString]; 
19                     break; 
20                 default: 
21                     break; 
22             } 
23  
24             dispatch_group_enter(downloadGroup); // 3 
25             Photo *photo = [[Photo alloc] initwithURL:url 
26                                   withCompletionBlock:^(UIImage *image, NSError *_error) { 
27                                       if (_error) { 
28                                           error = _error; 
29                                       } 
30                                       dispatch_group_leave(downloadGroup); // 4 
31                                   }]; 
32  
33             [[PhotoManager sharedManager] addPhoto:photo]; 
34         } 
35         dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5 
36         dispatch_async(dispatch_get_main_queue(), ^{ // 6 
37             if (completionBlock) { // 7 
38                 completionBlock(error); 
39             } 
40         }); 
41     }); 
42 } 

按照註釋的順序,你會看到:

1. 由於你在使用的是同步的 dispatch_group_wait ,它會阻塞當前線程,因此你要用 dispatch_async 將整個方法放入後臺隊列以免阻塞主線程。

2. 建立一個新的 Dispatch Group,它的做用就像一個用於未完成任務的計數器。

3. dispatch_group_enter 手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對出現,不然你可能會遇到詭異的崩潰問題。

4. 手動通知 Group 它的工做已經完成。再次說明,你必需要確保進入 Group 的次數和離開 Group 的次數相等。

5. dispatch_group_wait 會一直等待,直到任務所有完成或者超時。若是在全部任務完成前超時了,該函數會返回一個非零值。你能夠對此返回值作條件判斷以肯定是否超出等待週期;然而,你在這裏用 DISPATCH_TIME_FOREVER 讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,由於圖片的建立工做老是會完成的。

6. 此時此刻,你已經確保了,要麼全部的圖片任務都已完成,要麼發生了超時。而後,你在主線程上運行 completionBlock 回調。這會將工做放到主線程上,並在稍後執行。

7. 最後,檢查 completionBlock 是否爲 nil,若是不是,那就運行它。

 

編譯並運行你的應用,嘗試下載多個圖片,觀察你的應用是在什麼時候運行 completionBlock 的。

注意:若是你是在真機上運行應用,並且網絡活動發生得太快以至難以觀察 completionBlock 被調用的時刻,那麼你能夠在 Settings 應用裏的開發者相關部分裏打開一些網絡設置,以確保代碼按照咱們所指望的那樣工做。只需去往 Network Link Conditioner 區,開啓它,再選擇一個 Profile,「Very Bad Network」 就不錯。

 

若是你是在模擬器裏運行應用,你可使用 來自 GitHub 的 Network Link Conditioner 來改變網絡速度。它會成爲你工具箱中的一個好工具,由於它強制你研究你的應用在鏈接速度並不是最佳的狀況下會變成什麼樣。

目前爲止的解決方案還不錯,可是整體來講,若是可能,最好仍是要避免阻塞線程。你的下一個任務是重寫一些方法,以便當全部下載任務完成時能異步通知你。

 

在咱們轉向另一種使用 Dispatch Group 的方式以前,先看一個簡要的概述,關於什麼時候以及怎樣使用有着不一樣的隊列類型的 Dispatch Group :

1. 自定義串行隊列:它很適合當一組任務完成時發出通知。

2. 主隊列(串行):它也很適合這樣的狀況。但若是你要同步地等待全部工做地完成,那你就不該該使用它,由於你不能阻塞主線程。然而,異步模型是一個頗有吸引力的能用於在幾個較長任務(例如網絡調用)完成後更新 UI 的方式。

3. 併發隊列:它也很適合 Dispatch Group 和完成時通知。

 

Dispatch Group,第二種方式

上面的一切都很好,但在另外一個隊列上異步調度而後使用 dispatch_group_wait 來阻塞實在顯得有些笨拙。是的,還有另外一種方式……

 

在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的實現替換它:

 1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 
 2 { 
 3     // 1 
 4     __block NSError *error; 
 5     dispatch_group_t downloadGroup = dispatch_group_create();  
 6  
 7     for (NSInteger i = 0; i < 3; i++) { 
 8         NSURL *url; 
 9         switch (i) { 
10             case 0: 
11                 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 
12                 break; 
13             case 1: 
14                 url = [NSURL URLWithString:kSuccessKidURLString]; 
15                 break; 
16             case 2: 
17                 url = [NSURL URLWithString:kLotsOfFacesURLString]; 
18                 break; 
19             default: 
20                 break; 
21         } 
22  
23         dispatch_group_enter(downloadGroup); // 2 
24         Photo *photo = [[Photo alloc] initwithURL:url 
25                               withCompletionBlock:^(UIImage *image, NSError *_error) { 
26                                   if (_error) { 
27                                       error = _error; 
28                                   } 
29                                   dispatch_group_leave(downloadGroup); // 3 
30                               }]; 
31  
32         [[PhotoManager sharedManager] addPhoto:photo]; 
33     } 
34  
35     dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4 
36         if (completionBlock) { 
37             completionBlock(error); 
38         } 
39     }); 
40 } 

下面解釋新的異步方法如何工做:

1. 在新的實現裏,由於你沒有阻塞主線程,因此你並不須要將方法包裹在 async 調用中。

2. 一樣的 enter 方法,沒作任何修改。

3. 一樣的 leave 方法,也沒作任何修改。

4. dispatch_group_notify 以異步的方式工做。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那麼 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所須要的。

 

對於這個特定的工做,上面的處理明顯更清晰,並且也不會阻塞任何線程。

 

太多併發帶來的風險

既然你的工具箱裏有了這些新工具,你大概作任何事情都想使用它們,對吧?

看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已經注意到這裏的 for 循環,它迭代三次,下載三個不一樣的圖片。你的任務是嘗試讓 for 循環併發運行,以提升其速度。

 

dispatch_apply 恰好可用於這個任務。

 

dispatch_apply 表現得就像一個 for 循環,但它能併發地執行不一樣的迭代。這個函數是同步的,因此和普通的 for 循環同樣,它只會在全部工做都完成後纔會返回。

 

當在 Block 內計算任何給定數量的工做的最佳迭代數量時,必需要當心,由於過多的迭代和每一個迭代只有少許的工做會致使大量開銷以至它能抵消任何因併發帶來的收益。而被稱爲跨越式(striding)的技術能夠在此幫到你,即經過在每一個迭代裏多作幾個不一樣的工做。

譯者注:大概就能減小併發數量吧,做者是提醒你們注意併發的開銷,記在內心!

 

那什麼時候才適合用 dispatch_apply 呢?

1. 自定義串行隊列:串行隊列會徹底抵消 dispatch_apply 的功能;你還不如直接使用普通的 for 循環。

2. 主隊列(串行):與上面同樣,在串行隊列上不適合使用 dispatch_apply 。仍是用普通的 for 循環吧。

3. 併發隊列:對於併發循環來講是很好選擇,特別是當你須要追蹤任務的進度時。

 

回到 downloadPhotosWithCompletionBlock: 並用下列實現替換它:

 1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 
 2 { 
 3     __block NSError *error; 
 4     dispatch_group_t downloadGroup = dispatch_group_create(); 
 5  
 6     dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) { 
 7  
 8         NSURL *url; 
 9         switch (i) { 
10             case 0: 
11                 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 
12                 break; 
13             case 1: 
14                 url = [NSURL URLWithString:kSuccessKidURLString]; 
15                 break; 
16             case 2: 
17                 url = [NSURL URLWithString:kLotsOfFacesURLString]; 
18                 break; 
19             default: 
20                 break; 
21         } 
22  
23         dispatch_group_enter(downloadGroup); 
24         Photo *photo = [[Photo alloc] initwithURL:url 
25                               withCompletionBlock:^(UIImage *image, NSError *_error) { 
26                                   if (_error) { 
27                                       error = _error; 
28                                   } 
29                                   dispatch_group_leave(downloadGroup); 
30                               }]; 
31  
32         [[PhotoManager sharedManager] addPhoto:photo]; 
33     }); 
34  
35     dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ 
36         if (completionBlock) { 
37             completionBlock(error); 
38         } 
39     }); 
40 } 

你的循環如今是並行運行的了;在上面的代碼中,在調用 dispatch_apply 時,你用第一次參數指明瞭迭代的次數,用第二個參數指定了任務運行的隊列,而第三個參數是一個 Block。

 

要知道雖然你有代碼保證添加相片時線程安全,但圖片的順序卻可能不一樣,這取決於線程完成的順序。

 

編譯並運行,而後從 「Le Internet」 添加一些照片。注意到區別了嗎?

 

在真機上運行新代碼會稍微更快的獲得結果。但咱們所作的這些提速工做真的值得嗎?

 

實際上,在這個例子裏並不值得。下面是緣由:

1. 你建立並行運行線程而付出的開銷,極可能比直接使用 for 循環要多。若你要以合適的步長迭代很是大的集合,那才應該考慮使用 dispatch_apply。

2. 你用於建立應用的時間是有限的——除非實在太糟糕不然不要浪費時間去提早優化代碼。若是你要優化什麼,那去優化那些明顯值得你付出時間的部分。你能夠經過在 Instruments 裏分析你的應用,找出最長運行時間的方法。看看 如何在 Xcode 中使用 Instruments 能夠學到更多相關知識。

3. 一般狀況下,優化代碼會讓你的代碼更加複雜,不利於你本身和其餘開發者閱讀。請確保添加的複雜性能換來足夠多的好處。

 

記住,不要在優化上太瘋狂。你只會讓你本身和後來者更難以讀懂你的代碼。

 

 

 

 

原文連接:

http://www.cocoachina.com/industry/20140520/8485.html

http://www.cocoachina.com/applenews/devnews/2014/0428/8248.html

http://www.cocoachina.com/applenews/devnews/2014/0515/8433.html

http://blog.csdn.net/q199109106q/article/details/8565923

相關文章
相關標籤/搜索