iOS开发-多线程讲解3(GCD篇)

GCD

GCD(Grand Central Dispatch)

是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。前面也说过三种开发中GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的。对于熟悉C#异步调用的朋友对于GCD学习起来应该很快,因为它与C#中的异步调用基本是一样的。这种机制相比较于前面两种多线程开发方式最显著的优点就是它对于多核运算更加有效。

GCD中也有一个类似于NSOperationQueue的队列,GCD统一管理整个队列中的任务。但是GCD中的队列分为并行队列和串行队列两类:

串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。

并发队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。

其实在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务(从前面的演示中可以看到其实在NSOperation中也有一个主队列)。

串行队列

使用串行队列时首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行。下面使用线程队列演示图片的加载过程,你会发现多张图片会按顺序加载,因为当前队列中只有一个线程。

//

//  GCD实现多线程

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT

5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH

ROW_HEIGHT#defineCELL_SPACING 10

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSMutableArray *_imageNames;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView

*imageView=[[UIImageView

alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING),

r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH,

ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;//

imageView.backgroundColor=[UIColor redColor];[self.view

addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button

setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button

addTarget:self action:@selector(loadImageWithMultiThread)

forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];//创建图片链接_imageNames=[NSMutableArrayarray];for(inti=0; i

[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];

}

}#pragmamark 将图片显示到界面

-(void)updateImageWithData:(NSData *)data andIndex:(int)index{

UIImage *image=[UIImage imageWithData:data];

UIImageView *imageView= _imageViews[index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{

NSURL *url=[NSURL URLWithString:_imageNames[index]];

NSData *data=[NSData dataWithContentsOfURL:url];returndata;

}#pragmamark 加载图片

-(void)loadImage:(NSNumber

*)index{//如果在串行队列中会发现当前线程打印变化完全一样,因为他们在一个线程中NSLog(@"thread is

:%@",[NSThread currentThread]);inti=[index integerValue];//请求数据NSData

*data= [self requestData:i];//更新UI界面,此处调用了GCD主线程队列的方法dispatch_queue_t

mainQueue= dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{

[self updateImageWithData:data andIndex:i];

});

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;/*创建一个串行队列

第一个参数:队列名称

第二个参数:队列类型

*/dispatch_queue_t serialQueue=dispatch_queue_create("myThreadQueue1", DISPATCH_QUEUE_SERIAL);//注意queue对象不是指针类型

//创建多个线程用于填充图片for(inti=0; i

[self loadImage:[NSNumber numberWithInt:i]];

});

}//非ARC环境请释放

//    dispatch_release(seriQueue);}

@end

运行效果:

在上面的代码中更新UI还使用了GCD方法的主线程队列dispatch_get_main_queue(),其实这与前面两种主线程更新UI没有本质的区别。

并发队列

并发队列同样是使用dispatch_queue_create()方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT进行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是使用dispatch_get_global_queue()方法取得一个全局的并发队列(当然如果有多个并发队列可以使用前者创建)。下面通过并行队列演示一下多个图片的加载。代码与上面串行队列加载类似,只需要修改照片加载方法如下:

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;/*取得全局队列

第一个参数:线程优先级

第二个参数:标记参数,目前没有用,一般传入0

*/dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//创建多个线程用于填充图片for(inti=0; i

[self loadImage:[NSNumber numberWithInt:i]];

});

}

}

运行效果:

细心的朋友肯定会思考,既然可以使用dispatch_async()异步调用方法,是不是还有同步方法,确实如此,在GCD中还有一个dispatch_sync()方法。假设将上面的代码修改为同步调用,可以看到如下效果:

可以看点击按钮后按钮无法再次点击,因为所有图片的加载全部在主线程中(可以打印线程查看),主线程被阻塞,造成图片最终是一次性显示。可以得出结论:

在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行。

串行队列可以按顺序执行,并行队列的异步方法无法确定执行顺序。

UI界面的更新最好采用同步方法,其他操作采用异步方法。

其他任务执行方法

GCD执行任务的方法并非只有简单的同步调用方法和异步调用方法,还有其他一些常用方法:

dispatch_apply():重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。

dispatch_once():单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。

dispatch_time():延迟一定的时间后执行。

dispatch_barrier_async():

使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等待此方法中任务执

行后才能执行。(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用

dispatch_async()添加其他图片加载任务)

dispatch_group_async():实现对任务分组管理,如果一组任务全部完成可以通过dispatch_group_notify()方法获得完成通知(需要定义dispatch_group_t作为分组标识)。

线程同步

到多线程就不得不提多线程中的锁机制,多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有

锁机制往往会造成重大问题。举例来说,每年春节都是一票难求,在12306买票的过程中,成百上千的票瞬间就消失了。不妨假设某辆车有1千张票,同时有几

万人在抢这列车的车票,顺利的话前面的人都能买到票。但是如果现在只剩下一张票了,而同时还有几千人在购买这张票,虽然在进入购票环节的时候会判断当前票

数,但是当前已经有100个线程进入购票的环节,每个线程处理完票数都会减1,100个线程执行完当前票数为-99,遇到这种情况很明显是不允许的。

要解决资源抢夺问题在iOS中有常用的有两种方法:一种是使用NSLock同步锁,另一种是使用@synchronized代码块。两种方法实现原理是类似的,只是在处理上代码块使用起来更加简单(C#中也有类似的处理机制synchronized和lock)。

里不妨还拿图片加载来举例,假设现在有9张图片,但是有15个线程都准备加载这9张图片,约定不能重复加载同一张图片,这样就形成了一个资源抢夺的情况。

在下面的程序中将创建9张图片,每次读取照片链接时首先判断当前链接数是否大于1,用完一个则立即移除,最多只有9个。在使用同步方法之前先来看一下错误

的写法:

//

//  线程同步

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT

5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH

ROW_HEIGHT#defineCELL_SPACING 10#defineIMAGE_COUNT 9

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSMutableArray *_imageNames;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView

*imageView=[[UIImageView

alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING),

r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH,

ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;//

imageView.backgroundColor=[UIColor redColor];[self.view

addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button

setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button

addTarget:self action:@selector(loadImageWithMultiThread)

forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];//创建图片链接_imageNames=[NSMutableArrayarray];for(inti=0; i

[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];

}

}#pragmamark 将图片显示到界面

-(void)updateImageWithData:(NSData *)data andIndex:(int)index{

UIImage *image=[UIImage imageWithData:data];

UIImageView *imageView= _imageViews[index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{

NSData *data;

NSString *name;if(_imageNames.count>0) {

name=[_imageNames lastObject];

[_imageNames removeObject:name];

}if(name){

NSURL *url=[NSURL URLWithString:name];

data=[NSData dataWithContentsOfURL:url];

}returndata;

}#pragmamark 加载图片

-(void)loadImage:(NSNumber *)index{inti=[index integerValue];//请求数据

NSData *data= [self requestData:i];//更新UI界面,此处调用了GCD主线程队列的方法dispatch_queue_t mainQueue= dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{

[self updateImageWithData:data andIndex:i];

});

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;

dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//创建多个线程用于填充图片for(inti=0; i

[self loadImage:[NSNumber numberWithInt:i]];

});

}

}

@end

首先在_imageNames中存储了9个链接用于下载图片,然后在requestData:方法中每次只需先判断_imageNames的个数,如果大于一就读取一个链接加载图片,随即把用过的链接删除,一切貌似都没有问题。此时运行程序:

上面这个结果不一定每次都出现,关键要看从_imageNames读取链接、删除链接的速度,如果足够快可能不会有任何问题,但是如果速度稍慢就会出现上面的情况,很明显上面情况并不满足前面的需求。

分析这个问题造成的原因主:当一个线程A已经开始获取图片链接,获取完之后还没有来得及从_imageNames中删除,另一个线程B已经进入相应

代码中,由于每次读取的都是_imageNames的最后一个元素,因此后面的线程其实和前面线程取得的是同一个图片链接这样就造成图中看到的情况。要解

决这个问题,只要保证线程A进入相应代码之后B无法进入,只有等待A完成相关操作之后B才能进入即可。下面分别使用NSLock和

@synchronized对代码进行修改。

NSLock

iOS中对于资源抢占的问题可以使用同步锁NSLock来解决,使用时把需要加锁的代码(以后暂时称这段代码为”加锁代码“)放到NSLock的lock和unlock之间,一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。需要注意的是lock和unlock之间的”加锁代码“应该是抢占资源的读取和修改代码,不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。

另外,在上面的代码中”抢占资源“_imageNames定义成了成员变量,这么做是不明智的,应该定义为“原子属性”。对于被抢占资源来说将其定

义为原子属性是一个很好的习惯,因为有时候很难保证同一个资源不在别处读取和修改。nonatomic属性读取的是内存数据(寄存器计算好的结果),而

atomic就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只

有一个线程在访问一个属性。

下面的代码演示了如何使用NSLock进行线程同步:

KCMainViewController.h

//

//  KCMainViewController.h

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import@interfaceKCMainViewController : UIViewController

@property(atomic,strong) NSMutableArray *imageNames;

@end

KCMainViewController.m

//

//  线程同步

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT

5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH

ROW_HEIGHT#defineCELL_SPACING 10#defineIMAGE_COUNT 9

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSLock *_lock;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView

*imageView=[[UIImageView

alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING),

r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH,

ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;

[self.view addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button

setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button

addTarget:self action:@selector(loadImageWithMultiThread)

forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];//创建图片链接_imageNames=[NSMutableArrayarray];for(inti=0; i

[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];

}//初始化锁对象_lock=[[NSLock alloc]init];

}#pragmamark 将图片显示到界面

-(void)updateImageWithData:(NSData *)data andIndex:(int)index{

UIImage *image=[UIImage imageWithData:data];

UIImageView *imageView= _imageViews[index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{

NSData *data;

NSString *name;//加锁[_lock lock];if(_imageNames.count>0) {

name=[_imageNames lastObject];

[_imageNames removeObject:name];

}//使用完解锁[_lock unlock];if(name){

NSURL *url=[NSURL URLWithString:name];

data=[NSData dataWithContentsOfURL:url];

}returndata;

}#pragmamark 加载图片

-(void)loadImage:(NSNumber

*)index{inti=[index integerValue];//请求数据NSData *data= [self

requestData:i];//更新UI界面,此处调用了GCD主线程队列的方法dispatch_queue_t mainQueue=

dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{

[self updateImageWithData:data andIndex:i];

});

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;

dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//创建多个线程用于填充图片for(inti=0; i

[self loadImage:[NSNumber numberWithInt:i]];

});

}

}

@end

运行效果:

面也说过使用同步锁时如果一个线程A已经加锁,线程B就无法进入。那么B怎么知道是否资源已经被其他线程锁住呢?可以通过tryLock方法,此方法会返

回一个BOOL型的值,如果为YES说明获取锁成功,否则失败。另外还有一个lockBeforeData:方法指定在某个时间内获取锁,同样返回一个

BOOL值,如果在这个时间内加锁成功则返回YES,失败则返回NO。

@synchronized代码块

使用@synchronized解决线程同步问题相比较NSLock要简单一些,日常开发中也更推荐使用此方法。首先选择一个对象作为同步对象(一

般使用self),然后将”加锁代码”(争夺资源的读取、修改代码)放到代码块中。@synchronized中的代码执行时先检查同步对象是否被另一个

线程占用,如果占用该线程就会处于等待状态,直到同步对象被释放。下面的代码演示了如何使用@synchronized进行线程同步:

-(NSData *)requestData:(int)index{

NSData *data;

NSString *name;//线程同步@synchronized(self){if(_imageNames.count>0) {

name=[_imageNames lastObject];

[NSThread sleepForTimeInterval:0.001f];

[_imageNames removeObject:name];

}

}if(name){

NSURL *url=[NSURL URLWithString:name];

data=[NSData dataWithContentsOfURL:url];

}returndata;

}

扩展--使用GCD解决资源抢占问题

GCD中提供了一种信号机制,也可以解决资源抢占问题(和同步锁的机制并不一样)。GCD中信号量是dispatch_semaphore_t类型,支持

信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0

开始执行。根据这个原理我们可以初始化一个信号量变量,默认信号量设置为1,每当有线程进入“加锁代码”之后就调用信号等待命令(此时信号量为0)开始等

待,此时其他线程无法进入,执行完后发送信号通知(此时信号量为1),其他线程开始进入执行,如此一来就达到了线程同步目的。

//

//  GCD实现多线程--消息信号

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT

5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH

ROW_HEIGHT#defineCELL_SPACING 10#defineIMAGE_COUNT 9

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSLock *_lock;

dispatch_semaphore_t _semaphore;//定义一个信号量}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView

*imageView=[[UIImageView

alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING),

r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH,

ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;

[self.view addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button

setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button

addTarget:self action:@selector(loadImageWithMultiThread)

forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];//创建图片链接_imageNames=[NSMutableArrayarray];for(inti=0; i

[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];

}/*初始化信号量

参数是信号量初始值

*/_semaphore=dispatch_semaphore_create(1);

}#pragmamark 将图片显示到界面

-(void)updateImageWithData:(NSData *)data andIndex:(int)index{

UIImage *image=[UIImage imageWithData:data];

UIImageView *imageView= _imageViews[index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{

NSData *data;

NSString *name;/*信号等待

第二个参数:等待时间

*/dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);if(_imageNames.count>0) {

name=[_imageNames lastObject];

[_imageNames removeObject:name];

}//信号通知dispatch_semaphore_signal(_semaphore);if(name){

NSURL *url=[NSURL URLWithString:name];

data=[NSData dataWithContentsOfURL:url];

}returndata;

}#pragmamark 加载图片

-(void)loadImage:(NSNumber

*)index{inti=[index integerValue];//请求数据NSData *data= [self

requestData:i];//更新UI界面,此处调用了GCD主线程队列的方法dispatch_queue_t mainQueue=

dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{

[self updateImageWithData:data andIndex:i];

});

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;//

dispatch_queue_t

globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,

0);

//这里创建一个并发队列(使用全局并发队列也可以)dispatch_queue_t queue=dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);for(inti=0; i

dispatch_async(queue, ^{

[self loadImage:[NSNumber numberWithInt:i]];

});

}

}

@end

运行效果与前面使用同步锁是一样的。

扩展--控制线程通信

由于线程的调度是透明的,程序有时候很难对它进行有效的控制,为了解决这个问题iOS提供了NSCondition来控制线程通信(同前面GCD的

信号机制类似)。NSCondition实现了NSLocking协议,所以它本身也有lock和unlock方法,因此也可以将它作为NSLock解决

线程同步问题,此时使用方法跟NSLock没有区别,只要在线程开始时加锁,取得资源后释放锁即可,这部分内容比较简单在此不再演示。当然,单纯解决线程

同步问题不是NSCondition设计的主要目的,NSCondition更重要的是解决线程之间的调度关系(当然,这个过程中也必须先加锁、解锁)。

NSCondition可以调用wati方法控制某个线程处于等待状态,直到其他线程调用signal(此方法唤醒一个线程,如果有多个线程在等待则任意

唤醒一个)或者broadcast(此方法会唤醒所有等待线程)方法唤醒该线程才能继续。

假设当前imageNames没有任何图片,而整个界面能够加载15张图片(每张都不能重复),现在创建15个线程分别从imageNames中取

图片加载到界面中。由于imageNames中没有任何图片,那么15个线程都处于等待状态,只有当调用图片创建方法往imageNames中添加图片后

(每次创建一个)并且唤醒其他线程(这里只唤醒一个线程)才能继续执行加载图片。如此,每次创建一个图片就会唤醒一个线程去加载,这个过程其实就是一个典

型的生产者-消费者模式。下面通过NSCondition实现这个流程的控制:

KCMainViewController.h

//

//  KCMainViewController.h

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import@interfaceKCMainViewController : UIViewController#pragmamark 图片资源存储容器

@property(atomic,strong) NSMutableArray *imageNames;#pragmamark 当前加载的图片索引(图片链接地址连续)

@property(atomic,assign)intcurrentIndex;

@end

KCMainViewController.m

//

//  线程控制

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT

5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH

ROW_HEIGHT#defineCELL_SPACING 10#defineIMAGE_COUNT 9

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSCondition *_condition;

}

@end

@implementation KCMainViewController#pragmamark - 事件

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark - 内部私有方法#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView

*imageView=[[UIImageView

alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING),

r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH,

ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;

[self.view addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *btnLoad=[UIButton buttonWithType:UIButtonTypeRoundedRect];

btnLoad.frame=CGRectMake(50, 500, 100, 25);

[btnLoad setTitle:@"加载图片"forState:UIControlStateNormal];

[btnLoad addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:btnLoad];

UIButton *btnCreate=[UIButton buttonWithType:UIButtonTypeRoundedRect];

btnCreate.frame=CGRectMake(160, 500, 100, 25);

[btnCreate setTitle:@"创建图片"forState:UIControlStateNormal];

[btnCreate addTarget:self action:@selector(createImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:btnCreate];//创建图片链接_imageNames=[NSMutableArrayarray];//初始化锁对象_condition=[[NSCondition alloc]init];

_currentIndex=0;

}#pragmamark 创建图片

-(void)createImageName{

[_condition lock];//如果当前已经有图片了则不再创建,线程处于等待状态if(_imageNames.count>0) {

NSLog(@"createImageName wait, current:%i",_currentIndex);

[_condition wait];

}else{

NSLog(@"createImageName

work, current:%i",_currentIndex);//生产者,每次生产1张图片[_imageNames

addObject:[NSString

stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui

/613474/o_%i.jpg",_currentIndex++]];//创建完图片则发出信号唤醒其他等待线程[_condition

signal];

}

[_condition unlock];

}#pragmamark 加载图片并将图片显示到界面

-(void)loadAnUpdateImageWithIndex:

(int)index{//请求数据NSData *data= [self

requestData:index];//更新UI界面,此处调用了GCD主线程队列的方法dispatch_queue_t mainQueue=

dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{

UIImage *image=[UIImage imageWithData:data];

UIImageView *imageView= _imageViews[index];

imageView.image=image;

});

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{

NSData *data;

NSString *name;

name=[_imageNames lastObject];

[_imageNames removeObject:name];if(name){

NSURL *url=[NSURL URLWithString:name];

data=[NSData dataWithContentsOfURL:url];

}returndata;

}#pragmamark 加载图片

-(void)loadImage:(NSNumber

*)index{inti=(int)[index integerValue];//加锁[_condition

lock];//如果当前有图片资源则加载,否则等待if(_imageNames.count>0) {

NSLog(@"loadImage work,index is %i",i);

[self loadAnUpdateImageWithIndex:i];

[_condition broadcast];

}else{

NSLog(@"loadImage wait,index is %i",i);

NSLog(@"%@",[NSThread currentThread]);//线程等待[_condition wait];

NSLog(@"loadImage resore,index is %i",i);//一旦创建完图片立即加载[self loadAnUpdateImageWithIndex:i];

}//解锁[_condition unlock];

}#pragmamark - UI调用方法#pragmamark 异步创建一张图片链接

-(void)createImageWithMultiThread{

dispatch_queue_t

globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,

0);//创建图片链接dispatch_async(globalQueue, ^{

[self createImageName];

});

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;

dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);for(inti=0; i

[self loadImage:[NSNumber numberWithInt:i]];

});

}

}

@end

运行效果:

在上面的代码中loadImage:方法是消费者,当在界面中点击“加载图片”后就创建了15个消费者线程。在这个过程中每个线程进入图片加载方法之后都会先加锁,加锁之后其他进程是无法进入“加锁代码”的。但是第一个线程进入“加锁代码”后去加载图片却发现当前并没有任何图片,因此它只能等待。一旦调用了NSCondition的wait方法后其他线程就可以继续进入“加锁代码”(注意,这一点和前面说的NSLock、@synchronized等是不同的,使用NSLock、@synchronized等进行加锁后无论什么情况下,只要没有解锁其他线程就无法进入“加锁代码”),同时第一个线程处于等待队列中(此时并未解锁)。第二个线程进来之后同第一线程一样,发现没有图片就进入等待状态,然后第三个线程进入。。。如此反复,直到第十五个线程也处于等待。此时点击“创建图片”后会执行createImageName方法,这是一个生产者,它会创建一个图片链接放到imageNames中,然后通过调用NSCondition的signal方法就会在条件等待队列中选择一个线程(该线程会任意选取,假设为线程A)开启,那么此时这个线程就会继续执行。在上面代码中,wati方法之后会继续执行图片加载方法,那么此时线程A启动之后继续执行图片加载方法,当然此时可以成功加载图片。加载完图片之后线程A就会释放锁,整个线程任务完成。此时再次点击”创建图片“按钮重复前面的步骤加载其他图片。

为了说明上面的过程,这里以一个流程图的进行说明,流程图蓝色部分代表15个加载图片的线程,绿色部分表示创建图片资源线程。

iOS中的其他锁

在iOS开发中,除了同步锁有时候还会用到一些其他锁类型,在此简单介绍一下:

NSRecursiveLock

:递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递

归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

NSDistributedLock:分布锁,它本身是一个互斥锁,基于文件方式实现锁机制,可以跨进程访问。

pthread_mutex_t:同步锁,基于C语言的同步锁机制,使用方法与其他同步锁机制类似。

提示:在开发过程中除非必须用锁,否则应该尽可能不使用锁,因为多线程开发本身就是为了提高程序执行顺序,而同步锁本身就只能一个进程执行,这样不免降低执行效率。

总结

1>无论使用哪种方法进行多线程开发,每个线程启动后并不一定立即执行相应的操作,具体什么时候由系统调度(CPU空闲时就会执行)。

2>更新UI应该在主线程(UI线程)中进行,并且推荐使用同步调用,常用的方法如下:

- (void)performSelectorOnMainThread:(SEL)aSelector

withObject:(id)arg waitUntilDone:(BOOL)wait

(或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr

withObject:(id)arg waitUntilDone:(BOOL) wait;方法传递主线程[NSThread

mainThread])

[NSOperationQueue mainQueue] addOperationWithBlock:

dispatch_sync(dispatch_get_main_queue(), ^{})

3>NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。

4>对于简单的多线程开发建议使用NSObject的扩展方法完成,而不必使用NSThread。

5>可以使用NSThread的currentThread方法取得当前线程,使用 sleepForTimeInterval:方法让当前线程休眠。

6>NSOperation进行多线程开发可以控制线程总数及线程依赖关系。

7>创建一个NSOperation不应该直接调用start方法(如果直接start则会在主线程中调用)而是应该放到NSOperationQueue中启动。

8>相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。

9>NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD。

10>在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效。

11>在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行)。

12>相比使用NSLock,@synchronized更加简单,推荐使用后者

推荐阅读更多精彩内容