对iOS中几种锁的理解

概述:

在程序编程中,很多地方会涉及到多线程操作的编程。而在多线程中,就不得不说说其中"锁"的存在。多线程开发是为了发挥多核CPU的优势和防止因为单个线程的阻塞而造成整个系统的阻塞。而锁就是为了解决多线程在同时访问同一块资源时保持同步的方式。

常见的八大锁:

  1. OSSpinLock 自旋锁
  2. dispatch_semaphore 信号量实现加锁(GCD)
  3. pthread_mutex 互斥锁(C语言)
  4. NSCondition
  5. NSConditionLock 条件锁
  6. NSRecursiveLock 递归锁
  7. NSLock 对象锁
  8. @synchronized 关键字加锁

原理简介

1. OSSpinLock

OSSpinLock的一种自旋锁。原理是以一个标志位来标记该资源当前是否处在锁住(lock)的状态,如果是锁住(lock)的状态,那就会执行(do - while)死循环,这个过程称作忙等(busy-wait)。直到该资源被解锁(unlock),那么值钱一直在忙等状态下的线程,就会跳出循环,获得该资源并同时加锁(lock)。

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

上图中的伪代码,其实还存在一个问题:那就是如果一开始的时候同时有多个多线程执行 while 循环判断,此时它们拿到的标记位都是false,都不会被进入执行死循环进入忙等状态。这样就没法保证了锁的可靠性了。解决思路其实也是跟加锁思路一样,就是让申请锁的这一步为原子操作。保证此时只有一个线程在申请锁。

自旋锁的效率虽然很高,但是在有的场景下,并不是第一选择。比如在某些比较耗时的操作中,此时其它线程一直在做while循环操作,处于忙等状态。最终可能因为超时被系统释放,进入睡眠状态。显然中间忙等的过程被白白浪费了,是没有必要的。

OSSpinLock在特定情况下会出现优先级反转的问题,具体可以阅读ibireme大神的不再安全的 OSSpinLock的文章。笔者在这里简单概述一下:
在iOS系统中,苹果为了方便管理多线程,把多线程分为了5个程度的优先级:background,utility,default,user-initiated,user-interactive。并规定高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法就会产生潜在的优先级反转问题:
当一个低优先级的线程申请得到锁,并准备开始执行的时候,此时另一个高优先级的线程在循环中尝试申请获得锁,处于忙等状态中并占用大量的CPU时间。而此时获得锁的低优先级线程没法跟高优先级线程争夺CPU时间,无法完成任务、无法释放锁。
在iOS10以上苹果推出了 os_unfair_lock_t 来替代 OSSpinLock

image.png

2. dispatch_semaphore

dispatch_semaphore是在GCD中的一种同步方式,采用信号量的实现原理。主要有三个函数:

  1. dispatch_semaphore_create(long value):
    创建一个dispatch_semaphore_t 类型的信号量,且值为value。注意此处传入的value必须大于0,否则会返回null。
  2. dispatch_semaphore_signal(dispatch_semaphore_t signal)
    参入一个dispatch_semaphore_t 类型的信号量,并且使传入的信号量加1
  3. dispatch_semaphore_wait(dispatch_semaphore_t signal, dispatch_time_t timeout)
    参数是一个信号量和一个超时时间。函数内部会判断传入的信号量的值如果大于0,会继续执行下面的代码,并且将传入的信号量减1。如果函数内部判断传入的信号量的值等于0,则会进入waiting状态,主动让出时间片,如果在wai的过程中,其它信号发送了信号(执行了dispatch_semaphore_signal函数),就会继续执行下面的任务。如果在wait的过程中一直没有收到信号直到timeOut,也会继续执行下面的任务。看起来似乎跟NSLock一样,区别在于dispatch_semaphore保存了信号量这个参数,如果初始化设置信号量为x,那么就会同时有x个线程同时访问受保护区。而NSLock只能同一时间一个线程可以访问受保护区。
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
    dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(signal, timeOut);
        sleep(2);
        NSLog(@"操作1");
        dispatch_semaphore_signal(signal);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(signal, timeOut);
        sleep(1);
        NSLog(@"操作2");
        dispatch_semaphore_signal(signal);
    });

3. pthread_mutex

互斥锁,原理和信号量相似。遇到锁住的情况,会主动让出时间片,进入休眠状态,等待上下文切换唤醒。所以这种方式通常适合用在等待时间较长的场景下。因为系统上下文切换唤醒线程需要时间操作,通常在10微妙左右。如果等待时间比上下文切换的时间还短,使用忙等操作(busy-wait)比主动让出时间片进入睡眠更高效。
pthread_mutex有三种类型的锁:

  • PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
  • PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
  • PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。lock和unlock需要一一对应
  • PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
pthread_mutexattr_init(&attr);  
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
 
pthread_mutex_t mutex;  
pthread_mutex_init(&mutex, &attr) // 创建锁
 
pthread_mutex_lock(&mutex); // 申请锁  
    // 临界区
pthread_mutex_unlock(&mutex); // 释放锁 

4.@synchronize

这可能是我们用的最多的一种,写法简单,但是性能是最差的。atomic内部在setter和getter方法中就使用了它,使用它的时候,需要传入一个对象作为标记,底层实现原理是OC的runtime会为这个对象创建一个递归锁并存储在哈希表中,key就是该对象。传入的对象如果是nil的话,则不会得到任何锁,不会起到线程安全的作用。

4.其它

  1. NSLock实际上是在pthread_mutex基础上进行了封装了,且pthread_mutex的类型为 PTHREAD_MUTEX_ERRORCHECK,会有错误提示,同时会损失一定性能。
  2. NSRecursiveLock : 内部封装了 pthread_mutex ,类型为 PTHREAD_MUTEX_RECURSIVE。
  3. NSCondition :封装了一个互斥锁(pthread_mutex)和一个条件变量。
  4. NSConditionLock : 在NSCondition的基础上来实现的,内部持有一个NSCondition对象和一个condition_value属性。

参考文章:

ibiremed的不再安全的OSSpinLock
自旋锁和互斥锁的理解

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 5,423评论 0 6
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 3,940评论 0 0
  • 转载自:https://www.jianshu.com/p/938d68ed832c# 一、前言 前段时间看了几个...
    cafei阅读 9,964评论 1 12
  • 在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? ...
    IAMCJ阅读 8,377评论 2 25
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 4,193评论 0 2