多线程中的资源同步
2017-05-12
关于资源同步
所谓的资源同步就是在多个线程中,共享同一资源,由于每个线程都对这一公共的资源都有操作,所以有可能回发生:A线程正在利用资源r做操作时,而资源r却在此时被线程B所修改了。这就有可能会引起一系列的问题。所以资源同步就是指:当线程A利用资源r的时候,不允许其它线程访问资源r,这就是多线程中的资源同步。
- 在单线程中,资源一定是同步的
- 只有多线程中的共享资源(公共资源)才需要资源同步
资源同步发生的场景
在平常的编码中,当我们声明某一个变量时,我们首先要想到这个变量的使用场景:即是不是在多线程环境下,该变量(资源)是不是在不同的线程中都被操作(包括读取和修改),如果存在,那么就需要对这个资源做线程同步。
OC中的资源同步
OC中的资源同步有多种方式,下面我具体罗列出来:
- 用@synchronized 关键字
- 使用NSLock 加锁的方式(锁一定要成对出现,不然的话就会发生死锁)
- 把对共有资源的操作放到同一个串行队列中
资源同步问题场景
用多线程来模拟一个买票的程序,如果不加资源同步的话,买票的结果就是乱七八糟的,100张票有可能被卖了150次。在这里:
- 票的总张数是一定的
- 一个线程代表一个买票窗口
实现以上场景:
@interface ViewController ()
@property (nonatomic,assign) int ticketsCounts;  // 票总张数
@property (nonatomic,strong) NSThread *sellThread1;  // 卖票线程1
@property (nonatomic,strong) NSThread *sellThread2; // 卖票线程2
@property (nonatomic,strong) NSLock *ticketsLock;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.ticketsCounts = 100;
    
    self.sellThread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellWindow01_NotThreadSelf) object:nil];
    self.sellThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellWindow02_NotThreadSelf) object:nil];
    NSLog(@"开始卖票..");
    [self.sellThread1 start];
    [self.sellThread2 start];
    
    self.ticketsLock = [[NSLock alloc] init];
}
- (void)sellWindow01_NotThreadSelf
{
    while (self.ticketsCounts) {
        if (self.ticketsCounts == 0) {
            [self.sellThread1 cancel];
            NSLog(@"window01: 票卖完了... ");
            return;
        }
        self.ticketsCounts -= 1;
        NSLog(@"window01 卖了一张票,剩余票数:%d ",self.ticketsCounts);
        usleep(1000*1000); // 休眠1s
    }
}
- (void)sellWindow02_NotThreadSelf
{
    while (self.ticketsCounts) {
        
        if (self.ticketsCounts == 0) {
            [self.sellThread1 cancel];
            NSLog(@"window02: 票卖完了... ");
            return;
        }
        self.ticketsCounts -= 1;
        NSLog(@"window02 卖了一张票,剩余票数:%d ",self.ticketsCounts);
        usleep(1000*1000); // 休眠1s
        
    }
    
}
/*
  触摸屏幕停止卖票
 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.sellThread1 cancel];
    [self.sellThread2 cancel];
    NSLog(@"停止买票,现在剩余票数=%d",self.ticketsCounts);
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
我们看一下运行结果,很显然-剩余票数是混乱的。

解决资源不同步问题
我们这里只介绍在OC语言中资源同步的方法。
@synchronized
直接在卖票操作时,使用@synchronized关键字同步资源。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.ticketsCounts = 100;
        
    self.sellThread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellWindow01) object:nil];
    self.sellThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellWindow02) object:nil];
    NSLog(@"开始卖票..");
    [self.sellThread1 start];
    [self.sellThread2 start];
    
    
    self.ticketsLock = [[NSLock alloc] init];
}
- (void)sellWindow01
{
/* 同步代码块  */
    @synchronized (self) {
        
        while (self.ticketsCounts) {
            
            if (self.ticketsCounts == 0) {
                [self.sellThread1 cancel];
                NSLog(@"window01: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window01 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
            
        }
        
    }
}
- (void)sellWindow02
{
/** 同步代码块  **/
    @synchronized (self) {
        
        while (self.ticketsCounts) {
            
            if (self.ticketsCounts == 0) {
                [self.sellThread2 cancel];
                NSLog(@"window02: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window02 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
            
        }
        
    }
}
我们可以看到,此时资源就同步了。

NSLock
OC为我们提供了NSLoc,我们可以在对引发资源不同步的操作加上锁,这样也可以保证资源同步。
- (void)sellWindow01
{    
/** 加锁 **/
    [self.ticketsLock lock];
        while (self.ticketsCounts) {
            if (self.ticketsCounts == 0) {
                [self.sellThread1 cancel];
                NSLog(@"window01: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window01 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
        }
    [self.ticketsLock unlock];
    
}
- (void)sellWindow02
{    
 /** 加锁 **/
        [self.ticketsLock lock];
        while (self.ticketsCounts) {
            if (self.ticketsCounts == 0) {
                [self.sellThread2 cancel];
                NSLog(@"window02: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window02 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
        }
        [self.ticketsLock unlock];
    
}
利用宏定义,来简化加锁操作并带有日志:
 
#define TVULOCK(weblock)\
do{\
[weblock lock];\
NSLog(@"%s----lock-----%d",__func__,__LINE__);\
}while(0)
	
#define TVUUNLOCK(weblock)\
do{\
[weblock unlock];\
NSLog(@"%s----unlock-----%d",__func__,__LINE__);\
}while(0)
	
运行结果如图所示:

利用GCD串行队列
使用GCD串行队列也可以做资源同步。具体做法是:
- 创建一个串行队列
- 把公共代码放到串行队列中去执行
@property (nonatomic,strong) dispatch_queue_t phoneQueue;
- (dispatch_queue_t)phoneQueue
{
    if (!_phoneQueue) {
        _phoneQueue = dispatch_queue_create("phone_queue", DISPATCH_QUEUE_SERIAL);
    }
    return _phoneQueue;
}
- (void)sellWindow01
{      
    /** 放入串行队列中 **/
    dispatch_async(self.phoneQueue, ^{
        while (self.ticketsCounts) {
            
            if (self.ticketsCounts == 0) {
                [self.sellThread1 cancel];
                NSLog(@"window01: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window01 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
            
        }
    });
    
}
- (void)sellWindow02
{
    /** 放入串行队列中 **/
    
    dispatch_async(self.phoneQueue, ^{
        while (self.ticketsCounts) {
            if (self.ticketsCounts == 0) {
                [self.sellThread2 cancel];
                NSLog(@"window02: 票卖完了... ");
                return;
            }
            self.ticketsCounts -= 1;
            NSLog(@"window02 卖了一张票,剩余票数:%d ",self.ticketsCounts);
            usleep(1000*1000); // 休眠1s
        }
        
    });
    
}
运行结果如图所示:

