1、线程概述html
有些程序是一条直线,起点到终点——如简单的hello world,运行打印完,它的生命周期便结束了,像昙花一现。程序员
有些程序是一个圆,直到循环将它切断——像操做系统,一直运行,直到你关机。数据库
一个运行着的程序就是一个进程或者叫作一个任务,一个进程至少包含一个线程,线程就是程序的执行流。编程
Mac和IOS中的程序启动,建立好一个进程的同时,一个线程便开始运做,这个线程叫作主线程。主线程在程序中的位置和其余线程不一样,它是其余线程最终的父线程,且全部的界面的显示操做即AppKit或UIKit的操做必须在主线程进行。数组
系统中每个进程都有本身独立的虚拟内存空间,而同一个进程中的多个线程则公用进程的内存空间。xcode
每建立一个新的进程,都须要一些内存(如每一个线程有本身的stack空间)和消耗必定的CPU时间。安全
当多个进程对同一个资源出现争夺的时候须要注意线程安全问题。网络
建立线程:建立一个新的线程就是给进程增长一个执行流,因此新建一个线程须要提供一个函数或者方法做为线程的进口。多线程
概要提示:并发
iPhone中的线程应用并非无节制的,官方给出的资料显示,iPhone OS下的主线程的堆栈大小是1M,第二个线程开始就是512KB,而且该值不能经过编译器开关或线程API函数来更改,只有主线程有直接修改UI的能力。
2、简介
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