一、NSLock(普通锁)
NSLock 是对 pthread_mutex 普通锁的封装。
遵守 NSLocking 协议
它的两个方法
- (BOOL)tryLock; - (BOOL)lockBeforeDate:(NSDate *)limit;
(BOOL)lockBeforeDate:(NSDate *)limit;
传入一个时间,在这个时间之前如果能等到这把锁放开的话,就给这把锁加锁。如果时间没到,就一直在这里等。由于 NSLock 是互斥锁,所以等待的时间就是睡觉。如果时间到了,还没有等到锁。就加锁失败。
(BOOL)tryLock;
尝试加锁
NSLocking 协议里面有两个方法
/// 加锁 - (void)lock; - /// 解锁 - (void)unlock;
二、NSRecursiveLock(递归锁)
原理:
NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。
使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
应用:
NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。
这主要是用在循环或递归操作中。我们先来看一个示例:
NSLock *lock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveMethod)(int); RecursiveMethod = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value = %d", value); sleep(2); RecursiveMethod(value - 1); } [lock unlock]; }; RecursiveMethod(5); });
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息:
2016-11-06 15:18:32.865 SSS[19932:855235] value = 5 2016-11-06 15:18:34.938 SSS[19932:855235] -[NSLock lock]: deadlock (<NSLock: 0x6100000cc9b0> '(null)') 2016-11-06 15:18:34.938 SSS[19932:855235] Break on _NSLockError() to debug.
在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。
所以,对上面的代码进行一下改造
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
输出的结果如下:
2016-11-06 15:21:19.315 SSS[19972:857154] value = 5 2016-11-06 15:21:21.389 SSS[19972:857154] value = 4 2016-11-06 15:21:23.464 SSS[19972:857154] value = 3 2016-11-06 15:21:25.537 SSS[19972:857154] value = 2 2016-11-06 15:21:27.612 SSS[19972:857154] value = 1
三、OSSpinLock(自旋锁,弃用)
1、含义
OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
需要导入头文件 #import
2、主要代码
@property(nonatomic,assign) OSSpinLock sLock; self.sLock = OS_SPINLOCK_INIT; // 加锁, 因为要传的是指针,所以传入地址 OSSpinLockLock(&_sLock); // 尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true) BOOL result = OSSpinLockTry(&(_sLock)); // 解锁 OSSpinLockUnlock(&_sLock);
3、卖票示例
示例前准备:
/// 操作按钮 @property (nonatomic, strong) UIButton *btn; - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.btn]; } #pragma mark- Methods - (void)btnAction { } #pragma mark- Setting - (UIButton *)btn { if (!_btn) { _btn = [UIButton buttonWithType:UIButtonTypeCustom]; _btn.frame = CGRectMake(100, 100, 100, 30); _btn.backgroundColor = [UIColor lightGrayColor]; [_btn setTitle:@"LockTest" forState:UIControlStateNormal]; [_btn addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside]; } return _btn; }
场景演示代码:
/// 添加操作演示 - (void)btnAction { [self ticketTest]; } /// 卖票演示 - (void)ticketTest { dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // 窗口1 dispatch_async(queue, ^{ // 卖票 for (int i = 0 ; i < 3; i++) { [self saleTicket]; } }); // 窗口2 dispatch_async(queue, ^{ // 卖票 for (int i = 0 ; i < 3; i++) { [self saleTicket]; } }); // 窗口3 dispatch_async(queue, ^{ // 卖票 for (int i = 0 ; i < 3; i++) { [self saleTicket]; } }); }
没加锁代码:
/// 总票数 /// 操作按钮 @property (nonatomic, strong) UIButton *btn; /// 总票数 @property (nonatomic, assign) NSInteger ticketsCount; - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.btn]; self.ticketsCount = 15; } /// 添加操作演示 - (void)btnAction { [self ticketTest]; } /// 卖1张票 - (void)saleTicket { // 原来代码 if (self.ticketsCount > 0) { NSInteger oldTicketsCount = self.ticketsCount; sleep(.2); oldTicketsCount--; self.ticketsCount = oldTicketsCount; NSLog(@"还剩 %ld 张票 -%@", (long)oldTicketsCount, [NSThread currentThread]); } else { NSLog(@"票已卖完,建议购买下班车 -%@", [NSThread currentThread]); } }
控制台输出:
2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153535] 还剩 14 张票 -<NSThread: 0x282435280>{number = 25, name = (null)} 2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153177] 还剩 14 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)} 2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153708] 还剩 14 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)} 2021-03-26 11:03:38.882707+0800 LockDemo[37717:19153708] 还剩 13 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)} 2021-03-26 11:03:38.883040+0800 LockDemo[37717:19153535] 还剩 13 张票 -<NSThread: 0x282435280>{number = 25, name = (null)} 2021-03-26 11:03:38.883166+0800 LockDemo[37717:19153177] 还剩 13 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)} 2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153535] 还剩 12 张票 -<NSThread: 0x282435280>{number = 25, name = (null)} 2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153177] 还剩 12 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)} 2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153708] 还剩 12 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)}
加锁代码:
加锁: 别的线程无法再进行访问
解锁: 用完后,解除锁定,让其他线程可以访问。
如果使用须导入 #import
第一次加锁:在 saleTicket 方法中 ,一开始就加锁
演示前准备:
/// 操作按钮 @property (nonatomic, strong) UIButton *btn; /// 自旋锁 @property(nonatomic,assign) OSSpinLock sLock; /// 总票数 @property (nonatomic, assign) NSInteger ticketsCount; - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.btn]; self.sLock = OS_SPINLOCK_INIT; self.ticketsCount = 15; }
代码加锁:
/// 卖1张票 - (void)saleTicket { // 加锁, 因为要传的是指针,所以传入地址 OSSpinLockLock(&_sLock); // 原来代码 if (self.ticketsCount > 0) { NSInteger oldTicketsCount = self.ticketsCount; sleep(.2); oldTicketsCount--; self.ticketsCount = oldTicketsCount; NSLog(@"还剩 %ld 张票 -%@", (long)oldTicketsCount, [NSThread currentThread]); } else { NSLog(@"票已卖完,建议购买下班车 -%@", [NSThread currentThread]); } // 解锁 OSSpinLockUnlock(&_sLock); }
控制台输出:
2021-03-26 11:13:32.843142+0800 LockDemo[37750:19157258] 还剩 14 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:33.045948+0800 LockDemo[37750:19157258] 还剩 13 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:33.248671+0800 LockDemo[37750:19157258] 还剩 12 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:33.471973+0800 LockDemo[37750:19157646] 还剩 11 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)} 2021-03-26 11:13:33.677452+0800 LockDemo[37750:19157646] 还剩 10 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)} 2021-03-26 11:13:33.882731+0800 LockDemo[37750:19157646] 还剩 9 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)} 2021-03-26 11:13:34.115218+0800 LockDemo[37750:19157645] 还剩 8 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:34.318081+0800 LockDemo[37750:19157645] 还剩 7 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:34.519596+0800 LockDemo[37750:19157645] 还剩 6 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:36.509108+0800 LockDemo[37750:19157258] 还剩 5 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:36.714835+0800 LockDemo[37750:19157258] 还剩 4 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:36.920536+0800 LockDemo[37750:19157258] 还剩 3 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)} 2021-03-26 11:13:37.135940+0800 LockDemo[37750:19157645] 还剩 2 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:37.337543+0800 LockDemo[37750:19157645] 还剩 1 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:37.539328+0800 LockDemo[37750:19157645] 还剩 0 张票 -<NSThread: 0x283322180>{number = 26, name = (null)} 2021-03-26 11:13:37.576318+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)} 2021-03-26 11:13:37.576725+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)} 2021-03-26 11:13:37.576868+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)}
4、 锁原理分析
加锁本质就是线程阻塞,常见的线程阻塞有两种:
1、 相当于写了一个 while 循环,一直在循环代码
2、让线程直接睡眠
OSSpinLock 这个锁的线程阻塞就是忙等。
OSSpinLock 叫做”自旋锁”。等待别人把锁解开的时间中,一直占用着CPU 资源。这种状态我们叫做忙等,即一边忙着做事情,一边等待着解开锁。
其实就相当于写了一个while 循环。
相当于写了这样一行代码 while(锁还没被放开);
每次执行的时候,都会看下锁是否被放开,如果没有被放开,就执行一次。如果被放开了,就不在执行。
注意: 目前 OSSpinLock 已经不在安全,可能会出现优先级翻转问题。
例如:
我们有三个线程 , thread1,thread2,thread3。当我们支持多线程,并开启三条线程时,有可能3条线程同时做事情。那么系统是如何调度三条线程的?
流程分析:
1、安排时间给 三条线程,每条线程执行都执行一小会(也许三秒钟?)。例如,先给 thread1 3秒钟,在给 thread2 3秒钟,在给 thread3 3秒钟。然后在给 thread1 3秒钟 … … 一直这样,直到3个线程执行完毕。其实这是多线程的原理。
2、这也叫做 时间片轮转调度算法(操作系统在调度进程和线程时基本上都是按照上面的套路),它会牵扯到线程优先级问题,* 如果线程优先级高,给分配的时间就多
问题:
优先级翻转的问题。如thread1: 优先级高,thread2: 优先级低;
当 thread2 先进入 saleTicket 的锁中,就会先把OSSpinLockLock(&_lock) 加锁。这样当线程1 进入时,发现锁已经被加了。线程1 只能忙等。也就是在做 while(未解锁)这一行代码。
由于 thread1 的优先级高,很有可能 CPU 一直分配时间给 它,也就是说,CPU一直在做 while(未解锁)这一行代码。
也就是说,CPU 可能没有时间在 分配给 thread2 ,会照成 thread2 的线程不能再往下执行,也就永远无法放开这把锁。
这就是 自旋锁会发生的事情。
如果是 睡眠,就不会发生这种事情。
线程就不分配时间给thread1, thread2 就可以继续往下走,就可以完成解锁操作。然后 thread1 从休眠中唤醒,发现锁已经解开,thread1 就进行加锁,往下执行。
这就是为啥在 使用 OSSpinLockLock 的时候,苹果会提示 这个锁已经 不在使用。
还有更多,后面有时间加—
参考: