Posted by LML on March 24, 2020

前言

线程、队列、锁 与 Runloop 是iOS 中比较重要且相关联的概念。本文为此系列的第二篇,主要介绍与锁相关的知识,队列与线程相关知识件上篇;RunLoop 相关知识见下篇。 下面这张图大家应该很熟悉,本文主要按照这个顺序介绍几种常见的锁如何使用,锁的底层原理,为什么性能会不相同,后续有时间介绍

我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁应运而生。

1. OSSpinLock

OS_SPINLOCK_INIT: 默认值为 0,在 locked 状态时就会大于 0,unlocked状态下为 0
OSSpinLockLock(&oslock):上锁,参数为 OSSpinLock 地址
OSSpinLockUnlock(&oslock):解锁,参数为 OSSpinLock 地址
OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
当前线程锁失败,也可以继续其它任务,用 trylock 合适 当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock

__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 准备上锁");
    OSSpinLockLock(&oslock);
    sleep(4);
    NSLog(@"线程1");
    OSSpinLockUnlock(&oslock);
    NSLog(@"线程1 解锁成功");
    NSLog(@"--------------------------------------------------------");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 准备上锁");
    OSSpinLockLock(&oslock);
    NSLog(@"线程2");
    OSSpinLockUnlock(&oslock);
    NSLog(@"线程2 解锁成功");
});    

2. dispatch_semaphore 信号量

dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1
dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal 值 +1

dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 等待ing");
    dispatch_semaphore_wait(signal, overTime); //signal 值 -1
    NSLog(@"线程1");
    dispatch_semaphore_signal(signal); //signal 值 +1
    NSLog(@"线程1 发送信号");
    NSLog(@"--------------------------------------------------------");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 等待ing");
    dispatch_semaphore_wait(signal, overTime);
    NSLog(@"线程2");
    dispatch_semaphore_signal(signal);
    NSLog(@"线程2 发送信号");
});(@"线程2 解锁成功");
});     

dispatch_semaphore 信号量在使用上与其他的锁的不同在于

  • 其他锁大部分同一时间只能限制一个线程执行,但是信号量可以通过传入的值控制同时有一个线程执行
  • 可以设置timeout

    3. pthread_mutex

    ibireme 在 《不再安全的 OSSpinLock》 这篇文章中提到性能最好的 OSSpinLock 已经不再是线程安全.原因主要是:

    新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
    具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。
    苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。
    libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。
    最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。

上面的意思就是说:如果想要避免 OSSpinLock 不安全的问题,要么使用者自己保证:保证访问锁的线程全部都处于同一优先级,要么 更改 OSSpinLock 的实现,让其能兼容libobjc 的handoff lock 算法解决这个问题,但是前者一般不能保证,后者由于二进制兼容等问题OSSpinLock 也不能有改动。所以我们需要找别的锁替代 OSSpinLock。比如 pthread_mutex等其他锁(文章开头的其他锁都能代替OSSpinLock)。之所以在 pthread_mutex 这里介绍OSSpinLock 不在安全,是因为 dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。

使用方法:
声明一个互斥锁 pthread_mutex_t lock;
初始化. pthread_mutex_init(&lock, NULL);
上锁. pthread_mutex_lock(&lock). 解锁
pthread_mutex_unlock(&lock).
pthread_mutex_destroy(&lock).
pthread_mutex 中也有个pthread_mutex_trylock(&pLock),和上面提到的 OSSpinLockTry(&oslock)区别在于,前者可以加锁时返回的是 0,否则返回一个错误提示码;后者返回的 YES和NO。

⚠️

  • pthread_mutex_t 不是对象,pthread_mutex_t lock 即完成定义了一个锁,且该锁有占有内存,pthread_mutex_init只是完成了锁的初始化,并不是神奇内存。
  • 同样 pthread_mutex_destroy 并不是销毁内存,而是回复到调用 pthread_mutex_init 之前的状况。关于 pthread_mutex_destroy:
  • 内存如何销毁的,类似于 int 等类型
#import <pthread.h>
static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
 //1.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 准备上锁");
    pthread_mutex_lock(&pLock);
    sleep(3);
    NSLog(@"线程1");
    pthread_mutex_unlock(&pLock);
});

//1.线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 准备上锁");
    pthread_mutex_lock(&pLock);
    NSLog(@"线程2");
    pthread_mutex_unlock(&pLock);
});       

4.pthread_mutex(recursive)

pthread_mutex(recursive)递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。 下面例子,如果不是初始化为递归锁 PTHREAD_MUTEX_RECURSIVE ,会造成死锁

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用

//1.线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        pthread_mutex_lock(&pLock);
        if (value > 0) {
            NSLog(@"value: %d", value);
            RecursiveBlock(value - 1);
        }
        pthread_mutex_unlock(&pLock);
    };
    RecursiveBlock(5);
});  

5. NSRecursiveLock

递归锁,可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。和NSLock的使用一样,住不过是递归锁。

6. NSLock

NSLock *lock = [NSLock new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 尝试加速ing...");
    [lock lock];
    sleep(3);//睡眠5秒
    NSLog(@"线程1");
    [lock unlock];
    NSLog(@"线程1解锁成功");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 尝试加速ing...");
    BOOL x =  [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
    if (x) {
        NSLog(@"线程2");
        [lock unlock];
    }else{
        NSLog(@"失败");
    }
});  

lock、unlock:不多做解释,和上面一样 trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁**操作并返回 YES,反之返回 NO ⚠️:与前面几个不同,NSLock 是对象,不在是基本类型了

7. NSCondition

wait:进入等待状态
waitUntilDate::让一个线程等待一定的时间
signal:唤醒一个等待的线程
broadcast:唤醒所有等待的线程
遵循 NSLocking 协议,拥有lock unlock方法

//等待2秒
NSCondition *cLock = [NSCondition new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"start");
    [cLock lock];
    [cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
    NSLog(@"线程1");
    [cLock unlock];
});
//唤醒等待的线程
NSCondition *cLock = [NSCondition new];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lock];
    NSLog(@"线程1加锁成功");
    [cLock wait];
    NSLog(@"线程1");
    [cLock unlock];
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lock];
    NSLog(@"线程2加锁成功");
    [cLock wait];
    NSLog(@"线程2");
    [cLock unlock];
});
//唤醒一个等待的线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"唤醒一个等待的线程");
    [cLock signal];
});  
//唤醒所有线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"唤醒所有等待的线程");
    [cLock broadcast];
});

8. NSConditionLock

  • 以一个condition 初始化,后面trylock的时候必须是此condition才能lock,否则不能lock
  • unlockwithcondition 可以解锁,且更改condition
    • (void)lockWhenCondition:(NSInteger)condition;
      • The receiver’s condition must be equal to condition before the locking operation will succeed. This method blocks the thread’s execution until the lock can be acquired 获取锁之前一直被阻碍

9. @synchronized

10.小结

11.参考

https://www.cnblogs.com/kuliuheng/p/4080997.html 优先级反转
https://www.jianshu.com/p/8b8a01dd6356
https://juejin.im/post/5bf21d935188251d9e0c2937