美文网首页
iOS 开发中的锁

iOS 开发中的锁

作者: CoderLF | 来源:发表于2020-06-19 16:56 被阅读0次

如何深入理解 iOS 开发中的锁?

18569867-b138e26aac0802f1.png

本文会按照从上至下(速度由快至慢)的顺序分析每个锁的实现原理。需要说明的是,加解锁速度不表示锁的效率,只表示加解锁操作在执行时的复杂程度,下文会通过具体的例子来解释。

OSSpinLock

上述文章中已经介绍了 OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

为什么忙等会导致低优先级线程拿不到时间片?这还得从操作系统的线程调度说起。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。

自旋锁的实现原理

自旋锁的目的是为了确保临界区只有一个线程可以访问,它的使用可以用下面这段伪代码来描述:

do {  
    Acquire Lock
        Critical section  // 临界区
    Release Lock
        Reminder section // 不需要锁保护的代码
}

在 Acquire Lock 这一步,我们申请加锁,目的是为了保护临界区(Critical Section) 中的代码不会被多个线程执行。

自旋锁的实现思路很简单,理论上来说只要定义一个全局变量,用来表示锁的可用情况即可,伪代码如下:

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

注释写得很清楚,就不再逐行分析了。可惜这段代码存在一个问题: 如果一开始有多个线程同时执行 while 循环,他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了。解决思路也很简单,只要确保申请锁的过程是原子操作即可。

原子操作

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。

然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。

这些非常底层的概念无需完全掌握,我们只要知道上述申请锁的过程,可以用一个原子性操作 test_and_set 来完成,它用伪代码可以这样表示:

bool test_and_set (bool *target) {  
    bool rv = *target; 
    *target = TRUE; 
    return rv;
}

这段代码的作用是把 target 的值设置为 1,并返回原来的值。当然,在具体实现时,它通过一个原子性的指令来完成。

自旋锁的总结

至此,自旋锁的实现原理就很清楚了:

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {  
    while(test_and_set(&lock); // test_and_set 是一个原子操作
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的。

信号量

之前我在 介绍 GCD 底层实现的文章 中简单描述了信号量 dispatch_semaphore_t 的实现原理,它最终会调用到 sem_wait 方法,这个方法在 glibc 中被实现如下:

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 微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。

可以看到,自旋锁和信号量的实现都非常简单,这也是两者的加解锁耗时分别排在第一和第二的原因。再次强调,加解锁耗时不能准确反应出锁的效率(比如时间片切换就无法发生),它只能从一定程度上衡量锁的实现复杂程度。

相关文章

  • ios开发中的各种锁机制

    ios开发中的各种锁机制

  • iOS开发中的锁

    iOS开发中的锁 本人对锁没有深入理解,只是看了几篇文章,在这里做一下简单的总结。 iOS开发中,锁是用来解决线程...

  • 【iOS开发】iOS开发中的锁

    官方文档笔记疑问:1、NSLock与NSRecursiveLock 有什么区别?NSRecursiveLock 指...

  • [转]深入理解 iOS 开发中的锁

    转自(https://bestswifter.com/ios-lock/#) 深入理解 iOS 开发中的锁 摘要 ...

  • iOS知识整理(一)

    最近的iOS的一些知识进行复习,基本都是一些 blog iOS的各种锁 深入理解 iOS 开发中的锁 Objc_m...

  • iOS开发中的锁

    iOS 开发中的

  • iOS开发中的锁

    前言 在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。...

  • iOS 开发中的锁

    如何深入理解 iOS 开发中的锁? 本文会按照从上至下(速度由快至慢)的顺序分析每个锁的实现原理。需要说明的是,加...

  • iOS开发中的锁

    在开发中当遇到一些耗时的操作时我们经常会开启一个子线程,这样来提升执行效率、避免主线程卡顿。但是使用子线程时会涉及...

  • iOS开发中的锁

    锁的分类 根据资源申请者的表现形式,锁分为:自旋锁 和 互斥锁 自旋锁: 不引起调用者的休眠,如果自旋锁已经被别的...

网友评论

      本文标题:iOS 开发中的锁

      本文链接:https://www.haomeiwen.com/subject/fcgvxktx.html