为什么需要锁
在iOS中相信大家都用过多线程,多线程带来的好处显而易见,但是我们需要关注一下多线程有可能带来的问题。假设我们有一个这样的场景,我们有两条线程A和线程B,A线程做的事情是修改这个对象之后读取这个对象的数据,这个时候B线程可能也在修改这个对象。这个时候有两种情况(取决于B线程修改对象的时机):
- 正常的情况,A线程修改对象以及读取对象之后,B线程才开始修改这个对象。
- 异常的情况,A线程修改对象之后,B线程立刻修改了这个帝乡,然后A线程读取对象。这个时候A线程读取到的数据就出错了。
这就是我们常说的Data race,当两个线程同时在访问修改同一个块内存的时候,就有可能得到意想不到的结果。
基于上面的前提,我们在出现了用锁
来解决问题的方法。下面我们就来说说iOS中的锁。
在ibireme写的不再安全的OSSpinLock中给出了常用的锁的性能如下所示:
在解释下面的锁之前,我们先说说两种类型的锁,一种是自旋锁,一种是互斥锁。
按照功能来区分锁
互斥锁
互斥锁
是为了保护一个临界区或者资源不能同时被多个小城访问。当临界区加上互斥锁以后,其他的调用方不能获得锁,只有当互斥锁的持有方释放锁之后其他调用方才能获得锁。
如果调用方在获得锁的时候发现互斥锁
已经被其他方持有,那么该调用方只能进入睡眠状态,这样不会占用CPU资源。但是会有时间的消耗,系统的运行时基于CPU时间调度的,每次线程可能有100ms的运行时间,频繁的CPU切换也会消耗一定的时间。
自旋锁
自旋锁
和互斥锁相似,但是自旋锁不会引起休眠,当自旋锁被别的线程锁定的时候,那么调用方会一直处于等待的状态,用一种生活化的例子来说就像是上厕所,当你要上厕所发现里面已经有人的时候,你就会一直等在外面,直到他出来你就立刻抢占厕所。
由于调用方会一直循环看该自旋锁的的保持者是否已经释放了资源,所以总的效率来说比互斥锁高。但是自旋锁只用于短时间的资源访问,如果不能短时间内获得锁,就会一直占用着CPU,造成效率低下。
常见的锁的类型
OSSpinLock
OSSpinLock
是自旋锁,也正是由于它是自旋锁,所以容易发生优先级反转的问题。在ibireme的文章中已经写到,当一个低优先级线程获得锁的时候,如果此时一个高优先级的系统到来,那么会进入忙等状态,不会进入睡眠,此时会一直占用着系统CPU时间,导致低优先级的无法拿到CPU时间片,从而无法完成任务也无法释放锁。除非能保证访问锁的线程全部处于同一优先级,否则系统所有的自旋锁都会出现优先级反转的问题。现在苹果的OSSpinLock
已经被替换成os_unfair_lock
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
dispatch_semaphore
dispatch_semaphore
主要提供了三个函数:
1 | dispatch_semaphore_create(long value);//创造信号量 |
dispatch_semaphore
是GCD用来同步的一种方式,dispatch_semephore_create
方法用户创建一个dispatch_semephore_t
类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。dispatch_semaphore_wait()
方法是等待一个信号量,该方法会判断signal的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和NSCondition一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且dispatch_semaphore_signal
释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。
pthread-mutex
pthread-mutex
是互斥锁,互斥锁与信号量的机制非常相似,不会处于忙等状态,而是会阻塞线程并休眠。
pthread-mutex
提供了几个常用的方法
1 | int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);//初始化锁 |
pthread_mutex_init
方法用来初始化一个锁,需要传入一个pthread_mutex_t的对象,并且需要设置互斥锁的类型。互斥锁有四种类型:
1 | PTHREAD_MUTEX_NORMAL : 默认值普通锁,当一个线程加锁以后,其他线程进入按照优先顺序进入等待队列,并且解锁的时候按照先入先出的方式获得锁。 |
NSLock
NSLock
遵循NSLocking
协议,同时也是互斥锁,提供了lock和unlock方法来进行加锁和解锁。NSLock
内部是封装了pthread_mutext
,类型是PTHREAD_MUTEXT_ERRORCHECK
,它会损失一定的性能换来错误提示。
NSCondition
NSCondition
是封装了一个互斥锁和信号量,它把前者的lock以及后者的wait/signal统一到NSCondition
对象中,是基于条件变量pthread_cond_t
来实现的,和信号量相似,如果当前线程不满足条件,那么就会进入睡眠状态,等待其他线程释放锁或者释放信号之后,就会唤醒线程。类似于生产者和消费者模式
1 | NSCondition *lock = [[NSCondition alloc] init]; |
NSRecursiveLock
NSRecursiveLock
实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中
NSRecursiveLock
内部是通过pthread_mutex_lock来实现的,在内部会判断锁的类型,如果是递归锁,就允许递归调用,内部仅仅是将计数器+1。当调用unlock的时候,就将计数器减1。NSRecursiveLock内部使用的pthread_mutex_t的类型是PTHREAD_MUTEXT_RECURSIVE
NSConditionLock
NSConditonLock
是借助NSCondition,本质上是生产者-消费者模式,NSConditonLock
内部持有了一个NSCondition
对象和_condition_value
属性,当调用- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
初始化的时候会传入一个condition参数,该参数会赋值_condition_value
属性。
在
NSConditionLock
r中,对应的消费者就是- (void)lockWhenCondition:(NSInteger)condition;
方法,首先会调用[condition lock],然后开始进入阻塞状态,如果condition=_condition_value,那么就会休眠,直到代码调用- (void)unlockWithCondition:(NSInteger)condition;
才会唤起- (void)unlockWithCondition:(NSInteger)condition;
就是对应的生产者方法,内部会设置condition=_contion_value,并且发送广播告诉所有的消费者,表示生产完成,然后调用[condition unlock]释放锁。
@synchronized
@synchronized是OC层面上的锁,是所有的锁之中性能最差的。
@synchronized后面紧跟一个OC对象,实际上是将这个对象当做锁来使用。这是通过一个哈希表来实现的,OC在底层维护了一个互斥锁的数组,通过对象的哈希值去得到对象的互斥锁。
具体的实现原理可以参考萧玉大神的这篇文章: 关于 @synchronized,这儿比你想知道的还要多
总结
经过上面的分析我们知道锁的性能由高到低分别是OSSpinLock(已经不推荐使用)
->dispatch_semaphore
->pthread_mutext
->NSLock
->NSCondition
->NSRecursiveLock
->NSConditonLock
->@synchronized
我们再来梳理一下它们的关系:
dipatch_semaphore
是GCD同步的一种方式,通过dispatch_semaphore_t信号量来实现。
2.pthread_mutex
是互斥锁,提供了四种不同类型,不会像自旋锁一样忙等,而是会进入休眠等待。
3.NSLock
是封装了prthread_mutex
,锁的类型是PTHREAD_MUTEX_ERRORCHECK
,也就是当同一个线程获得同一个锁的时候,会返回错误。
4.NSCondition
是基于条件变量pthread_cond_t
实现的,和信号量相似,当不满足条件的时候就会进入休眠等待,知道condition对象发出signal信号,才会被唤醒执行。
5.NSRecursiveLock
是递归锁,同样是封装了pthread_mutex
来实现,但是锁的类型是PTHREAD_MUTEX_RECURSIVE
,允许统一递归获得锁,但是要注意加锁和解锁要一一对应。
6.NSConditionLock
是基于NSCondition
实现的,同样也是生产者和消费者模式。
7.@synchronized
是OC层面的锁,传入一个OC对象,通过对象的哈希值来作为标识符得到互斥锁,存入到一个数组里面。
参考
深入理解iOS中的锁
iOS中保证线程安全的几种方式与性能对比
iOS 常见知识点(三):Lock
不再安全的OSSpinLock