美文网首页iOS技术专题lock
iOS开发中自旋和互斥锁的理解以及所有锁的性能比较

iOS开发中自旋和互斥锁的理解以及所有锁的性能比较

作者: 笨坨 | 来源:发表于2018-08-07 15:10 被阅读61次

    补充:

    可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。

    可以看到YYKit组件中YYCache 和 YYImageCoder大量使用dispatch_semaphorepthread_mutex这两个锁

    OSSpinLock 自旋锁(虽然已经被证明不安全 优先级翻转),性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。

    dispatch_semaphore 是信号量,但当信号总量设为1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

    不存在等待的情况,例如不涉及到磁盘这种文件读写,dispatch_semaphore性能很高,如果涉及到的任务等待时间较长,就需要用pthread_mutex(OSSpinLock不安全就可以先不用了)

    前言

    写这个主要是整合下看到的资料,某天突然看到一到腾讯的面试题,里面就有一提就是让我们谈谈我们自己所认识的iOS中的锁,自己平时看到的就在RAC中的OSSPinLock,AF一些大型框架里面的disaptch_semaphore,还有就是NSLock和@synchronize这几个常见的锁了,用法是很简单,但是别人让你谈谈你自己的理解,因此就有了下面的资料,主要来自于一下几篇文章

    深入理解伪代码介绍

    YY大哥的不安全OSSPinLock

    简书小哥的用法介绍

    非常感谢这些大神的资料,对这个知识点有了基本的了解

    这里贴两个面试基础题目,对后续文章看起来会理解更好一点

    QA1:自旋和互斥对比?

    自旋锁和互斥锁

    相同点:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。

    不同点:

    互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

    自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。

    自旋锁的效率高于互斥锁。

    使用自旋锁时要注意:

    由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。

    持有自旋锁的线程在sleep之前应该释放自旋锁以便其他咸亨可以获得该自旋锁。内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起。

        使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:

            1.建立锁所需要的资源

            2.当线程被阻塞时所需要的资源

    两种锁的加锁原理:

    互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换(主动出让时间片,线程休眠,等待下一次唤醒),cpu的抢占,信号的发送等开销。

    自旋锁:线程一直是running(加锁——>解锁),死循环(忙等 do-while)检测锁的标志位,机制不复杂。

    介绍

    锁:在计算机科学中,锁是一种同步机制,用于在存在多线程的环境中实施对资源的访问限制。你可以理解成它用于排除并发的一种策略!你可以理解为为了防止多线程访问下资源的抢夺,保持线程同步的方式,以下就是常用的几个锁

    1.@synchronized 关键字加锁 

    2. NSLock 对象锁 

    3. NSCondition  

    4. NSConditionLock 条件锁 

    5. NSRecursiveLock 递归锁 

    6. pthread_mutex 互斥锁(C语言) 

    7. dispatch_semaphore 信号量实现加锁(GCD) 

    8. OSSpinLock 自旋锁 

    9.pthread_rwlock

    10.POSIX Conditions

    11.os_unfair_lock  iOS10之后替代OSSPinLock的锁,解决了优先级反转的问题

    这几个比较常见的基本用法可以看这里点击打开链接,这个这里就不介绍了,用法都很简单

    1.OSSPinLock

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

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

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

    忙等这种自旋锁的实现原理

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

    上面的伪代码就是实现自旋锁的基本原理,初始化一个lock的全局变量,一开始是false,while(lock)的意思是,当lock为true的时候,就进行忙等死循环(do-while申请锁),由于一开始是false,直接退出循环,然后lock锁上,执行临界区代码,也就是这个时候有其他线程访问,lock已经被锁上,while循环会一直忙等,处于申请锁状态,上一个锁执行完任务,就会解锁,这个时候lock变成了false,之前其他线程忙等状态下的条件变了,跳出循环,下一个线程执行lock=true,进门执行任务,其他线程继续等待。这里有个问题,如果一开始有多个线程同时执行 while 循环,他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了。解决思路也很简单,只要确保申请锁的过程是原子操作即可。

    用一个原子性操作 test_and_set 来完成,可以理解为线程进来的时候通过锁的上一个状态在判断一次,它用伪代码可以这样表示:

    该段代码的意思也很清晰,通过传入开关地址,先用局部变量存储一开始开关的状态,然后内部把开关变成开的状态,但是返回值是给while的,所以返回值就是开关进来没操作之前的状态。

    以下就是最终版本的自旋锁的伪代码:

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

    2.dispatch_semaphore(信号量  互斥类型)

    dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

    (1)dispatch_semaphore_create的声明为:

    dispatch_semaphore_t dispatch_semaphore_create(long value);

    传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

    值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

    (2)dispatch_semaphore_signal的声明为:

    long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

    这个函数会使传入的信号量dsema的值加1;

    (3) dispatch_semaphore_wait的声明为:

    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

    这个函数会使传入的信号量dsema的值减1;这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

    dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。

    信号量实现内部代码:

    首先会把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。具体的等待操作在lll_futex_wait函数中实现,lll是 low level lock 的简称。这个函数通过汇编代码实现,调用到SYS_futex这个系统调用,使线程进入睡眠状态,主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。

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

    3.pthread_mutex

    互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

    对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE等等,具体的特性就不做解释了,网上有很多相关资料。其中如果lock区域代码,再次进行加锁,就会变成递归,因此我们要把锁的属性变成PTHREAD_MUTEX_RECURSIVE来避免死锁

    互斥锁的实现

    互斥锁在申请锁时,调用了pthread_mutex_lock方法,它在不同的系统上实现各有不同,有时候它的内部是使用信号量来实现,即使不用信号量,也会调用到lll_futex_wait函数,从而导致线程休眠。

    上文说到如果临界区很短,忙等的效率也许更高,所以在有些版本的实现中,会首先尝试一定次数(比如 1000 次)的 testandtest,这样可以在错误使用互斥锁时提高性能。

    另外,由于pthread_mutex有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。

    4.NSLock,NSCondition,NSRecursiveLock

    这些只是上面几个的上层封装,可以看上面提供的文章,这里不展开了

    5.@synchronize (玉令天下传送门

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

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

    其实这个哈希表的实现和weak属性一样,这里是以对象为key,然后互斥锁的数组为value,weak属性是以对象为key,指向该对象的weak指针为数组value,当对象dealloc的时候,调用一系列函数,找到该对象的value数组,把数组指针都置为nil,然后再把key对应的记录全部清空。

    原文:https://blog.csdn.net/deft_mkjing/article/details/79513500

    相关文章

      网友评论

        本文标题:iOS开发中自旋和互斥锁的理解以及所有锁的性能比较

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