iOS知识梳理 - 多线程(3)锁

发布于 2019-10-24  约 12 分钟

多线程模型下,由于共享内存带来的冲突风险,锁是个避不开的话题。

关于锁

首先从平台无关的角度看,从能力上区分,主要有以下几种锁:

  1. 互斥锁(mutex):最普通的锁,阻塞等待,一种二元锁机制,只允许一个线程进入临界区
  2. 自旋锁(spin lock):能力上跟互斥锁一样的,只是它是忙等待
  3. 信号量(semaphore):信号量可以理解为互斥锁的推广形态,即互斥锁取值0/1,而信号量可以取更多的值,从而应对更复杂的同步。
  4. 条件锁(condition lock):有时候互斥的条件是复杂的而不是简单的数量上的竞争,此时可以用条件锁,条件锁的加锁解锁是通过代码触发的。
  5. 读写锁(read-write lock):顾名思义,就像文件读写,读操作之间并不互斥,但写操作与任何操作互斥。
  6. 递归锁(recursivelock):互斥锁的一个特例,允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。

所有的锁,语义上基本就这几种,

iOS中的锁

以API提供者的维度梳理一下iOS的锁

  1. 内核

    1. OSSpinLock:内核提供的自旋锁,已废弃
    2. os_unfair_lock:iOS10后官方推荐用来替代OSSpinLock的方案,性能很好
  2. pthread:POSIX标准真的是大而全...啥都有

    1. pthread_mutex:pthread的互斥锁
    2. pthread_rwlock:pthread的读写锁
    3. pthread_cond_t :pthread的条件锁
    4. sem_t:pthread的信号量
    5. pthread_spin_lock:pthread的自旋锁
  3. GCD

    1. dispatch_semaphore:gcd的信号量
  4. Cocoa Foundation

    1. NSLock:CF的互斥锁
    2. NSCondition:条件变量
    3. NSConditionLock:条件锁,在条件变量之上做了封装
    4. NSRecursiveLock
  5. objc runtime

    1. synchronized:本质上是pthread_mutex的上层封装,参考这里

以上,相对全面地列举了iOS中的锁,它们是不同层级的库提供的,但由于iOS中所有的线程本质上都是内核级线程,因此这些锁是能够公用的。

  1. 串行队列
  2. dispatch_barrier_async:栅栏函数,隔离前后的任务
  3. atomic

性能对比

环境:iPhone 7 plus + iOS 11

基于YY老师 不再安全的 OSSpinLock 中的性能对比代码,加入了os_unfair_lock,重新跑的一个性能对比。测试代码在这里
lock_benchmark.png

上面测试的是纯粹的加锁解锁性能,中间没有任何逻辑也不存在多线程抢占,为了更贴合我们的实际环境,我构造了一个简单的多线程环境:NSOperationQueue最大并发数为10,创建10个NSOperation,每个NSOperation做10w次i++操作,每次操作加锁,代码在这里,结果如下:

lock_benchmark2.png

可以看到多线程抢占的情形下结果跟前面略有不同,在真实业务场景下这个数据应该更有参考意义。

如何选择

由于OSSpinLock存在的优先级反转问题,已经废弃不再使用。(参考:不再安全的 OSSpinLock

  1. 一般场景,直接用@synchronized。使用最方便。一般业务开发场景,锁的性能影响不大,能力上也只需要简单的互斥锁,因此怎么方便怎么来。而且@synchronized性能也没有差太多。
  2. 性能苛刻的场景:os_unfair_lock,自旋锁废弃后官方推荐的替代品,性能优异。
  3. 需要信号量:dispatch_semaphore
  4. 需要条件锁:NSCondition
  5. 需要读写锁:pthread_rwlock
  6. 需要递归锁:NSRecursiveLock

使用

1. 自旋锁 OSSpinLock

自旋锁是这些锁中唯一一个依靠忙等待实现的锁,也就是说可以理解成一个暴力的while循环,因此会浪费较多的CPU,但它是所有锁中性能最高的。适用于对时延要求比较苛刻、临界区计算量比较小、本身CPU不存在瓶颈的场景。

但是现在不能用了。YY老师在不再安全的 OSSpinLock 中讲得很清楚了,当低优先级的线程已进入临界区,高优先级的线程想要获取资源就需要忙等待,占用大量CPU,导致低优先级线程迟迟不能执行完临界区代码,导致类死锁的问题。

OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
// do something
OSSpinLockUnlock(&lock);

2. os_unfair_lock

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock);
// do something
os_unfair_lock_unlock(&lock);

3. pthread_mutex

pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
// do something
pthread_mutex_unlock(&lock);

4. dispatch_semaphore

dispatch_semaphore_t lock =  dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(lock);

dispatch_semaphore_create传入参数是信号量的值,在这里就是能够同时进入临界区的线程数。dispatch_semaphore_wait,当信号量大于0时减一并进入临界区,如果信号量等于0则等待直到信号量不为0或到达设定时间。dispatch_semaphore_signal使信号量加1。

5. NSLock

NSLock *lock = [NSLock new];
[lock lock];
// do something
[lock unlock];

6. NSCondition

条件锁,以生产者消费者模型为例

NSCondition *condition = [NSCondition new];

// Thread 1: 消费者
- (void)consumer
{
  [condition lock];
  while(conditionNotSatisfied){
    [condition wait]
  }
  // 消费逻辑
  consume();
  [condition unlock];
}

// Thread 2: 生产者
- (void)producer
{
  [condition lock];
  // 生产逻辑
  produce();
  [condition signal];
  [condition unlock];
}

7. NSConditionLock

条件锁,跟NSCondition差不多,对条件做了封装,简化了使用但也没NSCondition那么灵活了。

NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];

// Thread 1: 消费者
- (void)consumer
{
  [condition lockWhenCondition:1];
  while(conditionNotSatisfied){
    [condition wait]
  }
  // 消费逻辑
  consume();
  [condition unlockWithCondition:0];
}

// Thread 2: 生产者
- (void)producer
{
  [condition lock];
  // 生产逻辑
  produce();
  [condition unlockWithCondition:1];
}

8. NSRecursiveLock

可以递归调用的互斥锁。

int i = 0;
NSRecursiveLock *lock = [NSRecursiveLock new];
- (void)testLock
{
    if(i > 0){
        [lock lock];
        [self testLock];
        i --;
        [lock lock];
    }
}

9. @synchronized

普通的锁,用着方便。

@synchronized(self) {
    // do something
}

10. pthread_rwlock

读写锁,一般也不怎么用得上,这里给了个字典set/get的例子,但是实际业务场景,通常普通的互斥锁就可以了。

在读操作比写操作多很多的情况下,读写锁的收益比较可观。

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
NSMutableDictionary *dic = [NSMutableDictionary new];
- (void)set
{
    // 写模式加锁
    pthread_rwlock_wrlock(&lock);
    dic[@"key"] = @"value";
    // 解锁
    pthread_rwlock_unlock(&lock);
}
- (NSString *)get
{
    NSString *value;
    // 写模式加锁
    pthread_rwlock_rdlock(&lock);
    value = dic[@"key"];
    // 解锁
    pthread_rwlock_unlock(&lock);
    return value;
}

推荐阅读

  1. 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么? - Tim Chen的回答 - 知乎
  2. 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么? - 胖君的回答 - 知乎
  3. 不再安全的 OSSpinLock
  4. iOS多线程安全-13种线程锁
  5. iOS开发中的11种锁以及性能对比 )
阅读 508发布于 2019-10-24

推荐阅读
二师兄的博客
用户专栏

个人博客,希望这次能多写点

3 人关注
41 篇文章
专栏主页
目录