多线程的安全隐患
一块资源可能会被多个线程共享,也就是说多个线程可能会访问同一块资源。
比如多个线程同时操作同一个对象,同一个变量。
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
比如一个买票问题:
#import "ViewController.h" @interface ViewController () @property (assign, nonatomic) NSInteger maxCount; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad];
_maxCount = 20; // 线程1 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; // 线程2 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; } - (void)run{ while (1) { if (_maxCount > 0) { // 暂停一段时间 [NSThread sleepForTimeInterval:0.002]; _maxCount--; NSLog(@"卖了一张票 - %ld - %@",_maxCount,[NSThread currentThread]); }else{ NSLog(@"票卖完了"); break; } } }
输出结果:
可以看到,当多个线程同时访问同一个数据的时候,很容易出现数据错乱,资源争夺的现象。
1. @synchronized(锁对象) { // 需要锁定的代码 } 来解决
互斥锁,使用的是线程同步的技术。加锁的代码需要尽量少,这个锁 ?? 对象需要保持在多个线程中都是同一个对象。
优点:是不需要显示的创建锁对象,就可以实现锁的机制。
缺点:会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁,会消耗系统资源。
- (void)run{ while (1) { // 这里使用self,或者一个全局对象也行,每个对象里面都有一把锁 @synchronized(self){ if (_maxCount > 0) { // 暂停一段时间 [NSThread sleepForTimeInterval:0.002]; _maxCount--; NSLog(@"卖了一张票 - %ld - %@",_maxCount,[NSThread currentThread]); }else{ NSLog(@"票卖完了"); break; } } } }
输出结果:
2. NSLock
在Cocoa框架中,NSLock实现了一个简单的互斥锁,所有锁(包括NSLock)的接口,实际上都是通过NSLocking协议定义的,它定义了 lock(加锁)和 unlock(解锁)方法。
不能多次调用lock方法,会造成死锁。
我们使用NSLock来解决上面的问题:
#import "ViewController.h" @interface ViewController () @property (assign, nonatomic) NSInteger maxCount; @property (strong, nonatomic) NSLock *lock; // 数据所 @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _maxCount = 20; _lock = [[NSLock alloc] init]; // 线程1 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; // 线程2 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; } - (void)run{ while (1) { // 加锁 [_lock lock]; if (_maxCount > 0) { // 暂停一段时间 [NSThread sleepForTimeInterval:0.002]; _maxCount--; NSLog(@"卖了一张票 - %ld - %@",_maxCount,[NSThread currentThread]); }else{ NSLog(@"票卖完了"); break; } // 释放锁 [_lock unlock]; } }
输出结果:
3. NSRecursiveLock 递归锁
如果在循环中,使用锁,很容易造成死锁。如下代码,在递归block中,多次的调用lock方法,锁会被多次的lock,所以自己也被阻塞了。
_lock = [[NSLock alloc] init]; //线程1 dispatch_async(dispatch_queue_create(NULL, 0), ^{ static void(^TestMethod)(int); TestMethod = ^(int value) { [_lock lock]; if (value > 0) { [NSThread sleepForTimeInterval:1]; NSLog(@"执行一次哦"); TestMethod(value--); } NSLog(@"是否执行到这里"); [_lock unlock]; }; TestMethod(5); });
输出内容:
2017-12-12 11:39:50.253155+0800 NSLock[1353:158620] 执行一次哦
此处将NSLock 换成 NSRecursiveLock 便可解决问题:
NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它会被多少次lock,每次成功的lock都必须平衡调用unlock操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其它线程获得。
_lock = [[NSRecursiveLock alloc] init]; //线程1 dispatch_async(dispatch_queue_create(NULL, 0), ^{ static void(^TestMethod)(int); TestMethod = ^(int value) { [_lock lock]; if (value > 0) { [NSThread sleepForTimeInterval:1]; NSLog(@"执行了一次哦"); TestMethod(value--); } NSLog(@"是否执行到这里"); [_lock unlock]; }; TestMethod(5); });
执行结果:
2017-12-12 11:49:43.378299+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:44.380543+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:45.382145+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:46.387148+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:47.388813+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:48.389408+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:49.392983+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:50.396521+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:51.399108+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:52.399976+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:53.404280+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:54.409044+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:55.412670+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:56.413754+0800 NSLock[1419:176157] 执行了一次哦 2017-12-12 11:49:57.414257+0800 NSLock[1419:176157] 执行了一次哦
4. NSConditionLock 条件锁
NSConditionLock相比NSLock多了一个condition参数,我们可以理解为一个条件标示。
@property (readonly)NSInteger condition; //这属性非常重要,外部传入的condition与之相同才会获取到lock对象,反之阻塞当前线程,直到condition相同 - (void)lockWhenCondition:(NSInteger)condition; //condition与内部相同才会获取锁对象并立即返回,否则阻塞线程直到condition相同 - (BOOL)tryLock;//尝试获取锁对象,获取成功需要配对unlock - (BOOL)tryLockWhenCondition:(NSInteger)condition; //同上 - (void)unlockWithCondition:(NSInteger)condition; //解锁,并且设置lock.condition = condition
NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0]; //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if([cLock tryLockWhenCondition:0]){ NSLog(@"线程1"); [cLock unlockWithCondition:1]; }else{ NSLog(@"失败"); } }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [cLock lockWhenCondition:3]; NSLog(@"线程2"); [cLock unlockWithCondition:2]; }); //线程3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [cLock lockWhenCondition:1]; NSLog(@"线程3"); [cLock unlockWithCondition:3]; });
输出结果:
2017-12-12 13:32:24.060013+0800 条件锁 - NSConditionLock[1783:250080] 线程1 2017-12-12 13:32:24.060461+0800 条件锁 - NSConditionLock[1783:250078] 线程3 2017-12-12 13:32:24.060626+0800 条件锁 - NSConditionLock[1783:250079] 线程2
- 我们在初始化这个lock的时候,给定了它的初始条件为0;
- 执行tryLockWhenCondition:时,我们传入的条件标示也是0,所以线程1加锁成功。
- 执行unLockWithCondition:时,这时候会把condition将0改为1
- 因为condition为1,所以先走线程3,然后线程3将condition修改为3,
- 最后走了线程2
可以看出,NSConditionLock还可以实现任务之间的依赖。
5. NSCondition
NSCondition实际上作为一个锁和一个线程检查器。锁主要是为了检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续进行下去,即线程是否被阻塞。
NSCondition实现了NSLocking协议,当多个线程访问同一段代码,会以wait为分水岭,一个线程等待另一个线程unlock后,才会走wait之后的代码。
[condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问 [condition unlock];//与lock 同时使用 [condition wait];//让当前线程处于等待状态 [condition signal];//CPU发信号告诉线程不用在等待,可以继续执行
我们可以来看一个生产者消费者的问题,
消费者取得锁,取产品,如果没有产品,则wait等待,这时候会释放锁,知道有线程去唤醒它去消费产品。
生产者制造产品,首先也要取得锁,然后生产,再发signal,这样可以唤醒wait的消费者
#import "ViewController.h" @interface ViewController () @property (strong, nonatomic) NSCondition *condition; @property (assign ,nonatomic) NSInteger goodNum; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; _goodNum = 0; _condition = [[NSCondition alloc] init]; [NSThread detachNewThreadSelector:@selector(buyGoods) toTarget:self withObject:nil]; [NSThread detachNewThreadSelector:@selector(shopGoods) toTarget:self withObject:nil]; } - (void)buyGoods{ [_condition lock]; while (_goodNum == 0){ NSLog(@"当前售卖个数为0,不能卖,等待"); [_condition wait]; } _goodNum--; NSLog(@"买了一个 - %ld",_goodNum); [_condition unlock]; } - (void)shopGoods{ [_condition lock]; _goodNum++; NSLog(@"准备卖一个 - %ld",_goodNum); [_condition signal]; NSLog(@"告诉买家可以买了"); [_condition unlock]; }
输出结果:
2017-12-12 14:43:48.995787+0800 NSCondition[2410:357340] 当前售卖个数为0,不能卖,等待 2017-12-12 14:43:48.996067+0800 NSCondition[2410:357341] 准备卖一个 - 1 2017-12-12 14:43:48.996578+0800 NSCondition[2410:357341] 告诉买家可以买了 2017-12-12 14:43:48.997806+0800 NSCondition[2410:357340] 买了一个 - 0
我们可以看出,当消费者想要买东西的时候,因为商品初始数量为0,所以只能等待生产者去生产商品,准备售卖,有商品之后,会signal,告诉消费者可以买了。
6. 使用C语言的pthread_mutex_t实现的锁
#import "ViewController.h" #import <pthread.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; __block pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ pthread_mutex_lock(&mutex); NSLog(@"线程1开始执行"); for (NSInteger i = 1; i <= 10; i++) { sleep(1); NSLog(@"线程1 在执行 ---- %ld",i); } pthread_mutex_unlock(&mutex); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ pthread_mutex_lock(&mutex); NSLog(@"线程1执行完了,线程2开始执行"); pthread_mutex_unlock(&mutex); }); }
输出结果:
2017-12-12 14:54:16.343540+0800 pthread_mutex - 互斥锁[2518:371518] 线程1开始执行 2017-12-12 14:54:17.348019+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 1 2017-12-12 14:54:18.348439+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 2 2017-12-12 14:54:19.351159+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 3 2017-12-12 14:54:20.355283+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 4 2017-12-12 14:54:21.358290+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 5 2017-12-12 14:54:22.361572+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 6 2017-12-12 14:54:23.362013+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 7 2017-12-12 14:54:24.363130+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 8 2017-12-12 14:54:25.366042+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 9 2017-12-12 14:54:26.370250+0800 pthread_mutex - 互斥锁[2518:371518] 线程1 在执行 ---- 10 2017-12-12 14:54:26.370496+0800 pthread_mutex - 互斥锁[2518:371519] 线程1执行完了,线程2开始执行