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

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

关于锁

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

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

全部的锁,语义上基本就这几种,git

iOS中的锁

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

  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 Foundationasync

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

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

以上,相对全面地列举了iOS中的锁,它们是不一样层级的库提供的,但因为iOS中全部的线程本质上都是内核级线程,所以这些锁是可以公用的。post

  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种锁以及性能对比 )
相关文章
相关标签/搜索