对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

对iOS中几种锁的理解_第1张图片
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中几种锁的理解)