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

相关文章
相关标签/搜索