聊一聊iOS中的锁和性能对比

一: 锁的种类

读写锁: atomic (iOS10之后采用os_unfair_lock,之前采用spinlock_t自旋锁) 
自旋锁: OSSpinLock (已废弃,不安全,会出现优先级反转问题)
互斥锁: pthread_mutex、@synchronized、NSLock
条件锁: NSConditionLock 、NSCondition
递归锁: NSRecursiveLock
信号量: dispatch_semaphore_t

二: 概念

1. 临界区: 指的是一块进行访问的代码
2. 自旋锁: 特点是在线程等待时会一直轮询,处于忙等状态,直到被锁资源释放锁。
3\. 互斥锁: 如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

三: 细说
3.1 OSSpinLock

3.1.1 OSSpinLock 是一种自旋锁
  由于它一直处于running的状态,给人感觉很耗CPU资源,但是它在互斥临界区计算量较小
的场景下,它的效率远高于其它的锁。这里为什么后面会讲解
3.1.2 OSSpinLock 不再安全?
    主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,
消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。
这种问题被称为优先级反转。
    举个例子说明:
      有高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。A 等待 C
 执行后的 Z,而 B 并不需要 Z,抢先获得时间片执行。C 由于没有时间片,无法执行(优先
级相对没有B高)。
      这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之
后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能
永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资
源。造成的后果,就是 A 无法获得 Z 而继续推进。)
    为什么忙等会导致低优先级线程拿不到时间片?
      操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。
每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己
的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
    如何解决优先级反转?
      1.优先级继承: 将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多
个线程等待,就取其中之一最高的优先级继承。
      2.优先级天花板: 则是直接设置优先级上限,给临界区一个最高优先级,进入临界区
的进程都将获得这个高优先级。
      3.禁止中断: 禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中
断的 。前者为一般任务运行时的优先级,后者为进入临界区的优先级。通过禁止中断来保护临
界区,没有其它第三种的优先级,也就不可能发生反转了。

3.1.3 实现原理
自旋锁的目的是为了确保临界区只有一个线程可以访问,看一段伪代码
      do {
           Acquire Lock 
              Critical section // 临界区 
           Release Lock 
              Reminder section // 不需要锁保护的代码
      }
在 Acquire Lock 这一步,我们申请加锁,目的是为了保护临界区(Critical Section) 中的代码不会被多个线程执行。
        bool lock = false;
        do {
            while(lock); //lock为ture一直循环
            lock = ture; //锁住,其它线程无法执行临界区
               Critical section // 临界区 
            lock = false; //释放锁
                Reminder section // 不需要锁保护的代码
        }

3.2 os_unfair_lock (互斥锁)

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的
系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,
而是等待线程会休眠。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

3.3 pthread_mutex_t(互斥锁)

pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。
pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行
线程上下文切换。

pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); 
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置类型,支持递归锁,条件锁等
pthread_mutex_init(&lock, &attr);//初始化
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock); //加锁
pthread_mutex_unlock(&lock);//释放锁

3.4 NSLock(pthread_mutex_t的封装)

 对pthread_mutex_t封装
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject  {
@private
    void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
tryLock 和 lock 方法都会请求加锁,唯一不同的是trylock在没有获得锁的时候可以继
续做一些任务和处理。lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,
没有拿到返回NO。 

3.5 NSCondition(条件锁,对pthread_mutex_t的封装)

同样是对pthread_mutex_t封装
@interface NSCondition : NSObject  {
@private
    void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,
waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,
broadcast是广播全部唤起。

看个例子
    NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *products = [NSMutableArray array];
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        if ([products count] == 0) {
            NSLog(@"wait for product");
            [condition wait];
        }
        [products removeObjectAtIndex:0];
        NSLog(@"custome a product");
        [condition unlock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product,总量:%zi",products.count);
        [condition signal];
        [condition unlock];
        sleep(1);
    });
输出
2021-08-01 19:29:22.456624+0800 UsuallyLockDemo[15160:419950] wait for product
2021-08-01 19:29:22.456905+0800 UsuallyLockDemo[15160:419948] produce a product,总量:1
2021-08-01 19:29:22.457068+0800 UsuallyLockDemo[15160:419950] custome a product

3.6 NSConditionLock(基于NSCondition)

 @interface NSConditionLock : NSObject  {
@private
    void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
例子
//发送信号条件锁 加载4张图片绘制
    NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
    NSMutableArray *products = [NSMutableArray array];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];
        for(int i=0;i<6;i++) {
            NSLog(@"load image count: %d",i);
            [products addObject:@(i)];
            if(products.count==3) {
                [conditionLock unlockWithCondition:3];
            }
        }
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        [conditionLock lockWhenCondition:3];
        NSLog(@"已经获取到4张图片->主线程渲染");
        [conditionLock unlock];
    });
输出:
2021-08-01 22:38:17.948660+0800 UsuallyLockDemo[19286:545978] load image count: 0
2021-08-01 22:38:17.948779+0800 UsuallyLockDemo[19286:545978] load image count: 1
2021-08-01 22:38:17.948873+0800 UsuallyLockDemo[19286:545978] load image count: 2
2021-08-01 22:38:17.948980+0800 UsuallyLockDemo[19286:545978] load image count: 3
2021-08-01 22:38:17.949078+0800 UsuallyLockDemo[19286:545978] load image count: 4
2021-08-01 22:38:17.949184+0800 UsuallyLockDemo[19286:545978] load image count: 5
2021-08-01 22:38:17.960780+0800 UsuallyLockDemo[19286:545426] 已经获取到4张图片->主线程渲染 

3.7 NSRecursiveLock(递归锁,基于pthread_mutex_t的封装)

 特点: 同一个线程可以加锁N次而不会引发死锁。递归锁会跟踪它被lock的次数。每次成功
的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,
以供其它线程使用

不会造成死锁例子:
NSRecursiveLock *relock = [[NSRecursiveLock alloc]init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static  void (^RecursiveLock)(int);
        RecursiveLock = ^(int value){
            [relock lock];
            if(value>0) {
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveLock(value - 1);
            }
            NSLog(@"解锁value = %d", value);
            [relock unlock];
        };
        RecursiveLock(3);
    });
输出:
2021-08-01 22:40:54.120858+0800 UsuallyLockDemo[19383:549318] value = 3
2021-08-01 22:40:54.120986+0800 UsuallyLockDemo[19383:549318] value = 2
2021-08-01 22:40:54.121076+0800 UsuallyLockDemo[19383:549318] value = 1
2021-08-01 22:40:54.121202+0800 UsuallyLockDemo[19383:549318] 解锁value = 0
2021-08-01 22:40:54.121305+0800 UsuallyLockDemo[19383:549318] 解锁value = 1
2021-08-01 22:40:54.121417+0800 UsuallyLockDemo[19383:549318] 解锁value = 2
2021-08-01 22:40:54.121515+0800 UsuallyLockDemo[19383:549318] 解锁value = 3 

3.8 dispatch_semaphore(信号量)

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。在 Dispatch 
Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或
大于 0 时,计数减 1 且不等待,可通过。
Dispatch Semaphore 提供了三个方法:
* dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
* dispatch_semaphore_signal:发送一个信号,让信号总量加 1
* dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待
(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续
执行,然后使用信号量。
Dispatch Semaphore 在实际开发中主要用于:
* 并发控制
* 保持线程同步,将异步执行任务转换为同步执行任务
* 保证线程安全,为线程加锁 

3.9 @synchronized (互斥锁,具体为递归锁)

@synchronized (self) {
}
转换成:
    @try {
        objc_sync_enter(obj);
        // do work
    } @finally {
        objc_sync_exit(obj)
    };
swift使用锁就是直接调用两个函数
int objc_sync_enter(id obj){
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        // 查找这个obj是否已经生成SyncData,如果没有生成一个
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock(); // 调用SyncData的递归锁加锁
    } else {
        // @synchronized(nil) does nothing
        // 如果传入nil, 打印了一个log,然后什么都不做
    if (DebugNilSync) {
        _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    return result;
}

通过代码可知: objc_sync_enter内部是一个递归锁实现
原理: @synchronized使用传入的object的内存地址作key,通过hash map对应
的一个系统维护的递归锁。所以不管是传入什么类型的object,只要是有内存地址,
就能启动同步代码块的效果。如果传入nil, 那就相当于没有加锁.

3.10 atomic(读写锁)

看一段源码:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    // 原子操作判断
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    objc_release(oldValue);
}
// atomic中用到的锁
using spinlock_t = mutex_tt;
// mutex_tt 的结构
class mutex_tt : nocopy_t {
    os_unfair_lock mLock;

iOS10之前代码:
typedef uintptr_t spin_lock_t;
OBJC_EXTERN void _spin_lock(spin_lock_t *lockp);
OBJC_EXTERN int  _spin_lock_try(spin_lock_t *lockp);
OBJC_EXTERN void _spin_unlock(spin_lock_t *lockp);
可以看出:atomic 原子操作只是对setter 和 getter 方法进行加锁
iOS之前:自旋锁spin_lock_t,之后互斥锁os_unfair_lock
atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是
安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter 
方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。
例子:
典型的 i = i+1;

四: 性能对比

对比图.png
1.自旋锁的实现
    bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
    do {  
        while(test_and_set(&lock); // test_and_set 是一个原子操作
            Critical section  // 临界区
        lock = false; // 相当于释放锁,这样别的线程可以进入临界区
            Reminder section // 不需要锁保护的代码        
    }
一直在循环查询锁的状态,效率非常高,但是如果临界区处理时间长,非常消耗CPU资源

2.信号量的底层实现

    int sem_wait (sem_t *sem) {  
        int *futex = (int *) sem;
        if (atomic_decrement_if_positive (futex) > 0)
            return 0;
        int err = lll_futex_wait (futex, 0);
            return -1;
    )
把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。
具体的等待操作在 lll_futex_wait 函数中实现,lll 是 low level lock 的简称。
这个函数通过汇编代码实现,调用到 SYS_futex 这个系统调用,使线程进入睡眠状态,
主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。

重点: 主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,
这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,
比如只有几个微秒,忙等就比线程睡眠更高效。

3.pthread_mutex

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 
表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,
需要进行上下文切换。

对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,
可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等

所以结论: pthread_mutex的性能肯定没有信号量好

4.NSLock

#define    MLOCK \n- (void) lock\n{\n  int err = pthread_mutex_lock(&_mutex);\n  // 错误处理 ……
    }
NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,
它会损失一定性能换来错误提示。
这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,
仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。

NSLock 比 pthread_mutex 略慢的原因在于它需要经过方法调用和错误提示,
同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

5.NSCondition

NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法
和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者:
  - (void) signal {
      pthread_cond_signal(&_condition);
  }
  // 其实这个函数是通过宏来定义的,展开后就是这样
  - (void) lock {
    int err = pthread_mutex_lock(&_mutex);
  }
  open func wait() {
      pthread_cond_wait(cond, mutex)
  }
它的加解锁过程与 NSLock 几乎一致,理论上来说耗时也应该一样(实际测试也是如此)

6.NSConditionLock

NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。
“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个
 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性
进行赋值:

    // 简化版代码
    - (id) initWithCondition: (NSInteger)value {
        if (nil != (self = [super init])) {
            _condition = [NSCondition new]
            _condition_value = value;
        }
        return self;
    }

它的 lockWhenCondition 方法其实就是消费者方法:
    - (void) lockWhenCondition: (NSInteger)value {
        [_condition lock];
        while (value != _condition_value) {
            [_condition wait];
        }
    }

对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了
所有的消费者:
    - (void) unlockWithCondition: (NSInteger)value {
        _condition_value = value;
        [_condition broadcast];
        [_condition unlock];
    }

7.NSRecursiveLock

递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,
如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型
不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE。

8.@synchronized (互斥锁->递归锁)

这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。

我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做
锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解
为锁池),通过对对象去哈希值来得到对应的互斥锁(递归锁)。

结合 dispatch_barrier_async 可以实现 频繁读,少量写操作
@interface Atme_Lock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 数据中心, 可能多个线程需要数据访问:
@property (nonatomic, strong) NSMutableDictionary *dataDic;
@end
- (id)init {
    self = [super init];
    if (self){
        // 创建一个并发队列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据字典:
        self.dataDic = [NSMutableDictionary dictionary];
    }
    return self;
}
#pragma mark - 读数据
- (id)get_objectForKey:(NSString *)key{
    __block id obj;
    // 同步读取指定数据:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataDic objectForKey:key];
    });
    return obj;
}
#pragma mark - 写数据
- (void)set_setObject:(id)obj forKey:(NSString *)key{
    // 异步栅栏调用设置数据:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataDic setObject:obj forKey:key];
    });
}

性能总结:

1.自旋锁->信号量->互斥锁->读写锁
2.NSLock->NSCondition->NSRecursiveLock->NSConditionLock

你可能感兴趣的:(聊一聊iOS中的锁和性能对比)