iOS 线程同步与锁

线程同步

说到多线程就不得不提多线程的锁机制,多线程操作过程中往往是多个线程并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个时候如果没有锁机制会造成很大的问题。举个例子比如买票系统比如只剩下最后一张票但是又100线程进入购票环节,每个线程处理完票数都要减1,100个线程处理完以后票数为-99,这肯定是不对的。
因此要解决资源抢夺问题在iOS中有两种方法:一种是NSLock同步锁,另一种是使用@synchronized代码块。两种方法原理类似,知识@synchronzied代码块处理起来更简单

举个例子:现在有九张图片,但是有15个线程都准备加载这9张图片,约定不能重复加载同一张图片,这样就会出现资源抢夺的情况。
下面是我写的一个例子

`#import “NSLockViewController.h”

define ROW_COUNT 5

define COLUMN_COUNT 3

define CELLSPACE 10

define IMAGE_COUNT 9

define ImageH [UIScreen mainScreen].bounds.size.width/3

@interface NSLockViewController (){
NSMutableArray *_imageViews;
NSMutableArray *_btnViews;
}

pragma mark

@property(nonatomic,strong)NSMutableArray *imageNames;
@end

@implementation NSLockViewController

  • (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self layOutUI];

    // Do any additional setup after loading the view.
    }
    -(void)layOutUI{
    //创建多个控件用于显示图片
    _imageViews = [[NSMutableArray alloc]init];

    for (int r = 0; r < ROW_COUNT; r++) {
    for (int c = 0; c < COLUMN_COUNT; c++) {
    UIImageView *imageView =[[UIImageView alloc]init];
    imageView.frame = CGRectMake(c%ROW_COUNT * ImageH, r * ImageH, ImageH, ImageH);
    [_imageViews addObject:imageView];
    [self.view addSubview:imageView];
    }
    }
    //创建多个按钮
    NSArray *arr = @[@”加载图片”,@”几种加锁方法”];
    _btnViews = [[NSMutableArray alloc]init];
    CGFloat BtnWIdth = [UIScreen mainScreen].bounds.size.width/arr.count;
    for(int i = 0; i < arr.count;i++){
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(i * BtnWIdth, 600,BtnWIdth, 20);
    [_btnViews addObject:button];
    [button setTitle:[arr objectAtIndex:i] forState:UIControlStateNormal];
    [button addTarget:self action:@selector(muchBtnClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];

    }
    for (int i = 0; i < IMAGE_COUNT; i++) {
    [self.imageNames addObject:[NSString stringWithFormat:@”http://pic.qiantucdn.com/58pic/12/62/61/88X58PICzpB.png“]];
    }
    }

pragma mark 按钮点击事件

-(void)muchBtnClick:(UIButton *)button{
NSInteger index = [_btnViews indexOfObject:button];
if (index == 0) {
//当前页面多线程加载图片
[self loadImageWithMutiThread];

}else if (1 == index){

}else if (2 == index){

}else{

}

}

pragma mark 图片数据

-(NSData *)requestData:(int)index{
NSData *data;
NSString *name;
if(self.imageNames.count > 0){
name = [self.imageNames lastObject];
[self.imageNames removeLastObject];
}
if (name) {
NSString *ImageStr = name;
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@”%@”,ImageStr]];
data = [NSData dataWithContentsOfURL:url];
}

return data;

}

pragma mark 加载图片

-(void)loadImage:(NSNumber *)index{
int i = [index intValue];
NSData *data = [self requestData:i];
dispatch_queue_t mainqueue = dispatch_get_main_queue();
dispatch_sync(mainqueue, ^{
UIImageView *imageView = _imageViews[i];
UIImage *image =[UIImage imageWithData:data];
imageView.image = image;
});

}

pragma mark 多线程加载图片

-(void)loadImageWithMutiThread{
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]];
});
}

}

pragma mark 懒加载

-(NSMutableArray *)imageNames{
if (_imageNames == nil) {
_imageNames = [[NSMutableArray alloc]init];
}
return _imageNames;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
`首先在_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就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只有一个线程在访问一个属性。
下面是代码

import “RealNSLockViewController.h”

define ROW_COUNT 5

define COLUMN_COUNT 3

define CELLSPACE 10

define IMAGE_COUNT 9

define ImageH [UIScreen mainScreen].bounds.size.width/3

@interface RealNSLockViewController (){
NSMutableArray *_imageViews;
NSLock *_lock;
}

pragma mark 所有图片URL

@property(atomic,strong)NSMutableArray *imageNames;

@end

@implementation RealNSLockViewController

  • (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self layoutUI];
    _lock = [[NSLock alloc]init];
    // Do any additional setup after loading the view.
    }

pragma mark 页面布局

-(void)layoutUI{
_imageViews = [[NSMutableArray alloc]init];
for (int row = 0; row < ROW_COUNT; row++) {
for (int col = 0; col < COLUMN_COUNT; col++) {
UIImageView *imageView =[[UIImageView alloc]init];
imageView.frame = CGRectMake(col%COLUMN_COUNT * ImageH, row * ImageH, ImageH, ImageH);
[_imageViews addObject:imageView];
[self.view addSubview:imageView];

    }
}
_imageNames = [NSMutableArray array];
//图片数据
for (int i = 0; i < IMAGE_COUNT; i++) {
    [self.imageNames addObject:[NSString stringWithFormat:@"http://pic.qiantucdn.com/58pic/12/62/61/88X58PICzpB.png"]];
}
//加载图片
UIButton *loadBtn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
loadBtn.frame = CGRectMake(50, 500, 100, 20);
[loadBtn setTitle:@"加载图片" forState:UIControlStateNormal];
[self.view addSubview:loadBtn];
[loadBtn addTarget:self action:@selector(LoadMuchThread) forControlEvents:UIControlEventTouchUpInside];

}

pragma mark 主线程加载

-(void)refreshImage:(NSData *)data AtIndex:(int)index{
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
UIImageView *imageView = _imageViews[index];
imageView.image = [UIImage imageWithData:data];
});
}

pragma mark 请求图片数据

-(NSData *)requestData:(int)index{
NSData *data;
NSString *name;
//加锁
[_lock lock];
if (_imageNames.count > 0) {
name = _imageNames.lastObject;
[_imageNames removeLastObject];
}else{

}
//解锁
[_lock unlock];
if (name != nil) {
    data = [NSData dataWithContentsOfURL:[NSURL URLWithString:name]];
}

return data;

}

pragma mark 加载图片

-(void)loadImage:(NSNumber *)number{
int index = [number intValue];
NSData *data = [self requestData:index];
[self refreshImage:data AtIndex:index];
}

pragma mark 创建多线程加载

-(void)LoadMuchThread{
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]];
    });
}

}

运行结果如下:
这里写图片描述
前面也说过使用同步锁时如果一个线程A已经加锁,线程B就无法进入。那么B怎么知道是否资源已经被其他线程锁住呢?可以通过tryLock方法,此方法会返回一个BOOL型的值,如果为YES说明获取锁成功,否则失败。另外还有一个lockBeforeData:方法指定在某个时间内获取锁,同样返回一个BOOL值,如果在这个时间内加锁成功则返回YES,失败则返回NO。

@synchronized代码块

用@synchronized解决线程同步问题相比较NSLock要简单一些,日常开发中也更推荐使用此方法。首先选择一个对象作为同步对象(一般使用self),然后将”加锁代码”(争夺资源的读取、修改代码)放到代码块中。@synchronized中的代码执行时先检查同步对象是否被另一个线程占用,如果占用该线程就会处于等待状态,直到同步对象被释放。下面的代码演示了如何使用@synchronized进行线程同步:
代码如下只需要修改一部分代码

pragma mark 请求图片数据

-(NSData *)requestData:(int)index{
NSData *data;
NSString *name;

pragma mark NSLock

/****
//加锁
 [_lock lock];
if (_imageNames.count > 0) {
    name = _imageNames.lastObject;
    [_imageNames removeLastObject];
}else{

}
//解锁
[_lock unlock];
 *****/

pragma mark @synchronized

@synchronized (self) {
    if (_imageNames.count > 0) {
        name = _imageNames.lastObject;
        [_imageNames removeLastObject];
    }
}

if (name != nil) {
    data = [NSData dataWithContentsOfURL:[NSURL URLWithString:name]];
}


return data;

}

这里写图片描述//运行结果如NSLOCk

使用GCD解决资源抢占问题

在GCD中提供了一种信号机制,也可以解决资源抢占问题(和同步锁的机制并不一样)。GCD中信号量是dispatch_semaphore_t类型,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。根据这个原理我们可以初始化一个信号量变量,默认信号量设置为1,每当有线程进入“加锁代码”之后就调用信号等待命令(此时信号量为0)开始等待,此时其他线程无法进入,执行完后发送信号通知(此时信号量为1),其他线程开始进入执行,如此一来就达到了线程同步目的。首先定义一个变量

代码如下
dispatch_semaphore_t _semaphore;//定义一个信号量
//初始化信号量
_semaphore = dispatch_semaphore_create(1);

pragma mark 请求图片数据

-(NSData *)requestData:(int)index{
NSData *data;
NSString *name;

pragma mark NSLock

/****
//加锁
 [_lock lock];
if (_imageNames.count > 0) {
    name = _imageNames.lastObject;
    [_imageNames removeLastObject];
}else{

}
//解锁
[_lock unlock];
 *****/

pragma mark @synchronized

/***
//代码块
@synchronized (self) {
    if (_imageNames.count > 0) {
        name = _imageNames.lastObject;
        [_imageNames removeLastObject];
    }
}
****/

pragma mark GCD 解决资源抢占

/*
 信号等待
 第二个参数等待时间
 */
dispatch_semaphore_wait(_semaphore,DISPATCH_TIME_FOREVER);
if(_imageNames.count > 0){
    name = _imageNames.lastObject;
    [_imageNames removeLastObject];
}
//信号通知
dispatch_semaphore_signal(_semaphore);
if (name != nil) {
    data = [NSData dataWithContentsOfURL:[NSURL URLWithString:name]];
}


return data;

}
//运行结果如上
这里写图片描述

扩展控制线程通信

由于线程的调度是透明的,程序有时候很难对它进行有效的控制,为了解决这个问题iOS提供了NSCondition来控制线程通信(同前面GCD的信号机制类似)。NSCondition实现了NSLocking协议,所以它本身也有lock和unlock方法,因此也可以将它作为NSLock解决线程同步问题,此时使用方法跟NSLock没有区别,只要在线程开始时加锁,取得资源后释放锁即可,这部分内容比较简单在此不再演示。当然,单纯解决线程同步问题不是NSCondition设计的主要目的,NSCondition更重要的是解决线程之间的调度关系(当然,这个过程中也必须先加锁、解锁)。NSCondition可以调用wati方法控制某个线程处于等待状态,直到其他线程调用signal(此方法唤醒一个线程,如果有多个线程在等待则任意唤醒一个)或者broadcast(此方法会唤醒所有等待线程)方法唤醒该线程才能继续。

假设当前imageNames没有任何图片,而整个界面能够加载15张图片(每张都不能重复),现在创建15个线程分别从imageNames中取图片加载到界面中。由于imageNames中没有任何图片,那么15个线程都处于等待状态,只有当调用图片创建方法往imageNames中添加图片后(每次创建一个)并且唤醒其他线程(这里只唤醒一个线程)才能继续执行加载图片。如此,每次创建一个图片就会唤醒一个线程去加载,这个过程其实就是一个典型的生产者-消费者模式。下面通过NSCondition实现这个流程的控制:
所有的代码实现都在我写的Demo里面,demo下载地址:http://download.csdn.net/detail/woshijunjunstudy/9833442

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值