iOS多线程开发资源抢夺和线程间的通讯问题
说到多线程就不得不提多线程中的锁机制,多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有锁机制往往会造成重大问题。举例来说,每年春节都是一票难求,在12306买票的过程中,成百上千的票瞬间就消失了。不妨假设某辆车有1千张票,同时有几万人在抢这列车的车票,顺利的话前面的人都能买到票。但是如果现在只剩下一张票了,而同时还有几千人在购买这张票,虽然在进入购票环节的时候会判断当前票数,但是当前已经有100个线程进入购票的环节,每个线程处理完票数都会减1,100个线程执行完当前票数为-99,遇到这种情况很明显是不允许的。
iOS提供了两种常用的解决资源抢夺的方法。一种是实用NSLock同步锁,另一种是实用@synchronized代码块。两种方法的实现原理是类似的只是处理上实用代码块更加简单。
这里不妨还拿图片加载来举例,假设现在有9张图片,但是有15个线程都准备加载这9张图片,约定不能重复加载同一张图片,这样就形成了一个资源抢夺的情况。在下面的程序中将创建9张图片,每次读取照片链接时首先判断当前链接数是否大于1,用完一个则立即移除,最多只有9个。
#define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 #define IMAGE_COUNT 9 @interface KCMainViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; } @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //创建多个图片控件用于显示图片 _imageViews=[NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { 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=[NSMutableArray array]; for (int i=0; i<IMAGE_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]]; } } #pragma mark 将图片显示到界面 -(void)updateImageWithData:(NSData *)data andIndex:(int )index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= _imageViews[index]; imageView.image=image; } #pragma mark 请求图片数据 -(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]; } return data; } #pragma mark 加载图片 -(void)loadImage:(NSNumber *)index{ int i=[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]; }); } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ int count=ROW_COUNT*COLUMN_COUNT; dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建多个线程用于填充图片 for (int i=0; i<count; ++i) { //异步执行队列任务 dispatch_async(globalQueue, ^{ [self loadImage:[NSNumber numberWithInt:i]]; }); } }
首先在_imageNames中存储了9个链接用于下载图片,然后在requestData:方法中每次只需先判断_imageNames的个数,如果大于一就读取一个链接加载图片。关键要看从_imageNames读取链接、删除链接的速度,如果足够快可能不会有任何问题,但是如果速度稍慢就会出现加载十五张图片的错误情况。
分析这个问题造成的原因主:当一个线程A已经开始获取图片链接,获取完之后还没有来得及从_imageNames中删除,另一个线程B已经进入相应代码中,由于每次读取的都是_imageNames的最后一个元素,因此后面的线程其实和前面线程取得的是同一个图片链接这样就造成图中看到的情况。要解决这个问题,只要保证线程A进入相应代码之后B无法进入,只有等待A完成相关操作之后B才能进入即可。下面分别使用NSLock和@synchronized对代码进行修改。
NSLock
iOS中对于资源抢占的问题可以使用同步锁NSLock来解决,使用时把需要加锁的代码(加锁代码)放到NSLock的Lock和unLock之间。一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程执行完之后。B线程才能访问加锁代码。需要注意的是Lock和unLock之间的加锁代码应该是抢占资源的读取和修改代码。不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
另外,在上面的代码中 抢占资源 _imageNames定义了成员变量 这么做是不明智的 应该定义为源自属性。对于被抢占资源来说将其定义为原子属性是一个很好的习惯,因为有时候很难保证同一个资源不在别处读取和修改。nonatomic属性读取的是内存数据(寄存器计算好的结果),而atomic就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只有一个线程在访问一个属性
下面的代码演示了如何使用NSLock进行线程同步:
#define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 #define IMAGE_COUNT 9 @interface ViewController () { NSMutableArray *_imageViews; NSLock *_lock; } @property (atomic,strong) NSMutableArray *imageNames; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } - (void)layoutUI { _imageViews = [NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { 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=[NSMutableArray array]; for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]]; } //初始化锁对象 _lock = [[NSLock alloc] init]; } - (void)loadImageWithMultiThread { int count = ROW_COUNT * COLUMN_COUNT; //创建一个串行队列 /* 第一个参数是 队列名称 第二个参数是 队列类型 */ //注意 dispatch_queue_t的对象不是指针类型 dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建多个线程用于填充图片 for (int i = 0; i < count; i++) { //异步执行队列任务 dispatch_async(globalQueue, ^{ [self loadImage:[NSNumber numberWithInt:i]]; }); } } - (void)loadImage:(NSNumber *)index { //如果在串行队列中会发现当前的线程打印完全一样 因为她们在一个线程中 NSLog(@"thread is: %@",[NSThread currentThread]); int i = (int)[index integerValue]; //请求数据 NSData *data = [self requestData:i]; //回到主线程更新UI dispatch_sync(dispatch_get_main_queue(), ^{ [self updateImageWithData:data andIndex:i]; }); } - (void)updateImageWithData:(NSData *)data andIndex:(int) index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= _imageViews[index]; imageView.image=image; } - (NSData *)requestData:(int)index { NSData *data; NSString *name; //加锁 [_lock lock]; if (_imageNames.count > 0) { name = [_imageNames lastObject]; [_imageNames removeObject:name]; } //使用完jiesuo [_lock unlock]; if (name) { NSURL *url=[NSURL URLWithString:name]; data=[NSData dataWithContentsOfURL:url]; } return data; }
前面也说过使用同步锁时如果一个线程A已经加锁,线程B就无法进入。那么B怎么知道是否资源已经被其他线程锁住呢?可以通过tryLock方法,此方法会返回一个BOOL型的值,如果为YES说明获取锁成功,否则失败。另外还有一个lockBeforeData:方法指定在某个时间内获取锁,同样返回一个BOOL值,如果在这个时间内加锁成功则返回YES,失败则返回NO。
@synchronized代码块
使用@synchronized解决线程同步问题相对比较NSLock简单一点,日常开发中爷推荐使用该方法。首先选择一个对象作为同步对象,一般选择self,然后将 加锁代码 放到代码块中。@synchronized中的代码执行时先检查同步对象是否被另一个线程占用。如果被占用该线程就会处于等待状态。直到同步对象被释放。下面的代码演示了如何使用@synchronized进行线程同步。
- (NSData *)requestData:(int)index { NSData *data; NSString *name; //加锁 // [_lock lock]; // if (_imageNames.count > 0) { // name = [_imageNames lastObject]; // [_imageNames removeObject:name]; // } // //使用完jiesuo // [_lock unlock]; //同步线程 @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]; } return data; }
http://www.cocoachina.com/ios/20160707/16957.html
使用GCD解决资源抢占问题
在GCD中提供了一种信号机制,也可以枪战资源抢夺的问题(和同步锁的极致并不一样)。GCD 中信号量是dispatch_semaphore_t类型,支持信号通知和信号等待等。每当发送一个信号通知,信号量加一;每当发送一个等待信号则信号量减一。如果信号量为0则信号会处于等待状态,直到信号大于0才开始执行。根据这个原理我们可以初始化一个信号量变量,默认信号量设置为1,每当有线程进入加锁代码之后 就调用信号等待命令(此时信号量为0)开始等待。此时其他线程无法进入 执行完毕后发送信号通知(此时信号量为1)其他线程开始进入执行,如此一来就达到了线程同步的目的。
- (NSData *)requestData:(int)index { NSData *data; NSString *name; //加锁 // [_lock lock]; // if (_imageNames.count > 0) { // name = [_imageNames lastObject]; // [_imageNames removeObject:name]; // } // //使用完jiesuo // [_lock unlock]; //同步线程 // @synchronized(self) { // if (_imageNames.count > 0) { // name = [_imageNames lastObject]; // [NSThread sleepForTimeInterval:0.001f]; // [_imageNames removeObject:name]; // } // } //初始化信号量 _semaphore = dispatch_semaphore_create(1); //信号等待 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]; } return data; }
总结
1.无论使用哪种方法进行多线程开发,每个线程启动后并不一定立即执行相应的操作,具体什么时候由系统调度。
2.更新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则会在主线程中执行)
8.相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。
9.NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD。
10.在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效。
11.在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行)。
12.关于线程同步 抢夺资源的问题@synchronized最简单,但是dispatch_semaphore_t更加高效。
附加iOS中保证线程安全的几种方式的性能对比
http://www.cocoachina.com/ios/20160707/16957.html