一、线程概述
有些程序是一条直线,起点到终点——如简单的hello world,运行打印完,它的生命周期便结束了,像昙花一现。
有些程序是一个圆,直到循环将它切断——像操作系统,一直运行,直到你关机。
一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。
Mac和IOS中的程序启动,创建好一个进程的同时,一个线程便开始运作,这个线程叫做主线程。主线程在程序中的位置和其他线程不同,它是其他线程最终的父线程,且所有的界面的显示操作即AppKit或UIKit的操作必须在主线程进行。
系统中每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则公用进程的内存空间。
每创建一个新的进程,都需要一些内存(如每个线程有自己的stack空间)和消耗一定的CPU时间。
当多个进程对同一个资源出现争夺的时候需要注意线程安全问题。
创建线程:创建一个新的线程就是给进程增加一个执行流,所以新建一个线程需要提供一个函数或者方法作为线程的进口。
概要提示:
iPhone中的线程应用并不是无节制的,官方给出的资料显示,iPhone OS下的主线程的堆栈大小是1M,第二个线程开始就是512KB,并且该值不能通过编译器开关或线程API函数来更改,只有主线程有直接修改UI的能力。
二、简介
iOS有三种多线程编程的技术,分别是:
(一)NSThread
(二)Cocoa NSOperation
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:
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];
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
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我已经注释了。
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 }
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];
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 }
运行可以看到下载图片显示在界面上。
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 NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]); 3 }]; 4 5 [operation addExecutionBlock:^() { 6 NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]); 7 }]; 8 9 [operation addExecutionBlock:^() { 10 NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]); 11 }]; 12 13 [operation addExecutionBlock:^() { 14 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 }
并发行和并行性的区别可以用馒头做比喻。前者相当于一个人同时吃三个馒头和三个人同时吃一个馒头。
并发性(Concurrence):指两个或两个以上的事件或活动在同一时间间隔内发生。并发的实质是物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
并行性(parallelism)指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
区别:一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。
前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生。
两者的联系:并行的事件或活动一定是并发的,但反之并发的事件或活动未必是并行的。并行性是并发性的特例,而并发性是并行性的扩展。
所有的调度队列(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的使用:
用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();
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被打印。
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
请注意执行的时间,可以看到执行的顺序如上所述。
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 }
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 - (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 }
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 }
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 }
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 });
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 }
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 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。
需要记住的是,这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。这些需要用其它方式来保证线程安全,例如同步访问数据,你将在下面几个小节看到。
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 }
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 - (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 + (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 }
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 - (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 }
纠正过早弹出的提示
你可能已经注意到当你尝试用 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