什么会给多线程的安全造成隐患?
有了多线程技术支持,我们可以并发的进行多个任务,因此同一块资源就有可能在多个线程中同时被访问(读/写)。这个现象叫作资源共享,比如多个线程同时访问了同一个对象,同一个变量或同一个文件,这样就有可能引发数据错乱何数据安全问题。
经典问题一——存钱取钱下面通过代码展示一下该问题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self moneyTest];
}
//存钱取钱测试
-(void)moneyTest {
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i= 0; i<10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
[self drawMoney];
}
});
}
-(void)saveMoney {
NSInteger oldMoney = self.money;
sleep(.2);//模拟任务时长,便于问题显现
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存了50元,账户余额%ld-------%@",(long)oldMoney, [NSThread currentThread]);
}
-(void)drawMoney {
NSInteger oldMoney = self.money;
sleep(.2);//模拟任务时长,便于问题显现
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取了20元,账户余额%ld-------%@",(long)oldMoney, [NSThread currentThread]);
}
我们在moneyTest
方法中,以多线程方式分别进行了10
次的存钱取钱操作,每次存50
,每次取20
,存款初值为100
,目标余额应该为100+ (50*10) - (20*10) = 400
,运行结果如下
可以看出最后的余额数不对。
经典问题二——卖票问题下面通过代码展示一下该问题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self sellTicketTest];
}
//卖票问题
-(void)sellTicketTest {
self.ticketsCount = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i= 0; i<5; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<5; i++) {
[self sellTicket];
}
});
}
-(void)sellTicket {
NSInteger oldTicketsCount = self.ticketsCount;
sleep(.2);//模拟任务时长,便于问题显现
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%ld张票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
}
在sellTicketTest
里面,起始票数30
,通过3条线程同时卖票,每条线程卖10
张,最有应该全部卖完才对,运行程序,结果如下
打印结果看出最后的剩余票数发生了错误。
上述两个经典问题都是由于多条线程对同一资源进行了读写操作而导致的。用一个大家都熟悉的图片来表示就是
针对这个问题的解决方案:使用线程同步
技术(同步,就是协同步调,按预定的先后次序进行)。常见的线程同步技术就是:加锁。
先用下图概括一下
线程同步(加锁)方案
iOS中的线程同步方案有如下几种:
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
@synchronized
下面我们来依次体验一下。
(一)OSSpinLock
OSSpinLock
叫作”自旋锁“,需要导入头文件#import <libkern/OSAtomic.h>
。它有如下API
OSSpinLock lock = OS_UNFAIR_LOCK_INIT;
——初始化锁对象lockOSSpinLockTry(&lock);
——尝试加锁,加锁成功继续,加锁失败返回,继续执行后面的代码,不阻塞线程OSSpinLockLock(&lock);
——加锁,加锁失败会阻塞线程,进行等待OSSpinLockUnlock(&lock);
——解锁
下面来看一下代码中如何使用它。以下代码承接上面的卖票案例进行加锁操作
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketsCount;
@end
*******************************************************
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self sellTicketTest];
// [self moneyTest];
}
//卖票问题
-(void)sellTicketTest {
self.ticketsCount = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i= 0; i<10; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
[self sellTicket];
}
});
}
-(void)sellTicket {
//初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁????
OSSpinLockLock(&lock);
NSInteger oldTicketsCount = self.ticketsCount;
sleep(.2);//模拟任务时长,便于问题显现
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%ld张票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
//解锁????
OSSpinLockUnlock(&lock);
}
@end
运行后结果结果看没成功,怎么回事呢?这里补充一下加锁的原理:一张图搞定
所以根据上图的原理,我们应该使用同一个锁对象来给某个操作(代码段)加锁。所以将上面的锁写成一个全局性质的属性即可,代码如下
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketsCount;
@property (nonatomic, assign) OSSpinLock lock;
@end
*******************************************************
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self sellTicketTest];
}
//卖票问题
-(void)sellTicketTest {
self.ticketsCount = 30;
//初始化锁
self.lock = OS_SPINLOCK_INIT;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i= 0; i<10; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
[self sellTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i<10; i++) {
[self sellTicket];
}
});
}
-(void)sellTicket {
//加锁????
OSSpinLockLock(&_lock);
NSInteger oldTicketsCount = self.ticketsCount;
sleep(.2);//模拟任务时长,便于问题显现
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%ld张票-------%@",(long)oldTicketsCount, [NSThread currentThread]);
//解锁????
OSSpinLockUnlock(