ios:线程锁

简介: NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。

一、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 的时候,苹果会提示 这个锁已经 不在使用。


还有更多,后面有时间加—


参考:


1、线程锁-NSRecursiveLock

2、多线程六——加锁方案一 : OSSpinLock


3、线程锁- @synchronized

4、线程锁-NSConditionLock

5、zhuge1127的博客


相关文章
|
1月前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
10天前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
2月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
41 6
|
2月前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
3月前
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
55 2
|
3月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
55 0
|
3月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
32 0
|
3月前
|
Java API
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
46 0
|
3月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
59 0
|
3月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解