iOS开发中的锁

2018-02-09 12:48:28来源:https://www.jianshu.com/p/db17700875c9作者:赵梦楠人点击

分享


前言

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






概述

锁是多线程开发中最基本的同步工具。开发中常用的锁通常分为以下几种类型:


Mutex(互斥锁): 互斥锁是一种信号量,一次只能访问一个线程如果一个互斥体正在使用,而另一个线程试图获取它,则该线程将阻塞,直到互斥体被其原始持有者释放。如果多个线程竞争同一个互斥体。则一次只允许一个线程访问它。


Recursive lock(递归锁):递归锁是互斥锁的变体。递归锁允许单个线程在释放之前多次获取锁。其他线程保持阻塞状态。直到锁的所有者释放锁的次数与获取它的次数相同,递归锁主要在递归中使用,但也可能在多个方法需要单独获取锁的情况下使用。


Spin lock(自旋锁): 自旋锁重复其锁定条件,直到该条件成立。自旋锁最常用于多处理器系统,其中锁的预期等待时间很短。在这些情况下,轮询通常比阻塞线程更高效,这涉及到上下文切换和线程数据结构的更新。


Read-write lock(读写锁):读写锁也被称为Shared-exclusive lock。通常用于较大规模的操作,适用于数据结构被频繁读取和偶尔修改,可以显着提高性能。在正常操作期间,多个线程可以同时访问数据。当一个线程想要写入数据时会阻塞,直到所有的读取线程释放锁。此时,写入线程才能获取锁,并修给数据。写入线程在锁定时,新的线程将被阻塞,直到写入线程完成。


Distributed lock(分布式锁): 分布式锁提供进程级别的互斥访问。与真正的互斥锁不同,分布式锁不会阻塞进程或阻止进程运行。它只是报告锁何时忙,让流程决定如何进行。



Double-checked lock(双重检查锁): 双重检查锁试图通过在锁定之前测试锁定标准来降低锁定的开销。由于双重检查的锁可能是不安全的,系统不提供对它们的明确的支持,并且它们的使用是不鼓励的。

以上大致介绍了锁的分类,下面将介绍Objective-C中各种实现锁的方式。


一、@synchronized指令
简介

@synchronized指令是Objective-C中易用性和可读性最好的创建互斥锁的方式。我们不用去直接创建锁和锁定对象,它会像其它互斥锁一样,防止其它线程获取同一个锁。传递给@synchronized的对象是区分保护块的唯一标识。它的简单用法是这样


- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
//需要加锁的内容
}
}

实现原理

编译器将@synchronized转化成了一对objc_sync_enter()objc_sync_exit()的调用,通过查看源码我们可以分析得出:



@synchronized是通过recursive_mutex_t递归锁实现;
@synchronized(object)中传入的object的内存地址,被用作唯一的key,通过hash map对应到一个系统维护的递归锁;
objc_sync_enter()objc_sync_exit()并没有对传入的对象做retainsreleases;
objc_sync_enter(nil)objc_sync_exit(nil)不起任何作用;

详细的分析可以参见这篇博客:正确使用多线程同步锁@synchronized()。


使用注意

通过上面的分析, 我们可以得出@synchronized使用中应该注意的几个问题


因为@synchronized使用递归锁实现的,所以如下代码不会产生死锁;
@synchronized (obj) {
NSLog(@"1st sync");
@synchronized (obj) {
NSLog(@"2nd sync");
}
}

因为是利用传入的object的内存地址作为唯一标识,所以传入的object理论上可以是任意对象,但应避免不同的critical section使用相同的锁,应该是不同的数据使用不同的锁;
@synchronized (objectA) {
[arrA addObject:obj];
}
@synchronized (objectB) {
[arrB addObject:obj];
}

应该注意传入对象的生命周期,因为@synchronized(object)并没有对object进行retains,因此当@synchronized(nil)时,将不起任何作用;


注意@synchronized(object)内部的方法调用,将不需要同步操作的方法放在外面调用;


另外官方文档中提到 :



作为预防措施,@synchronized块隐式地向受保护的代码添加异常处理程序。如果引发异常,该处理程序会自动释放互斥锁。这意味着为了使用@synchronized指令,还必须在代码中启用Objective-C异常处理。如果您不想由隐式异常处理程序引起额外开销,则应考虑使用锁类。



二、pthread_mutex
简介

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。相关函数:


__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_destroy(pthread_mutex_t *);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
int * __restrict);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_init(pthread_mutex_t * __restrict,
const pthread_mutexattr_t * _Nullable __restrict);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_lock(pthread_mutex_t *);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
int * __restrict);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_trylock(pthread_mutex_t *);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_unlock(pthread_mutex_t *);


pthread_mutex 可通过宏定义静态初始化pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
动态方式是采用int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)函数来初始化互斥锁,mutexattr用于指定属性,如果为NULL则使用缺省属性。
int pthread_mutex_destroy(pthread_mutex_t *mutex)函数用于销毁一个互斥锁,这意味着释放它所占用的资源,且要求锁当前处于开放状态。
pthread_mutexPTHREAD_MUTEX_NORMAL(普通互斥锁)、PTHREAD_MUTEX_ERRORCHECK(检错锁)和PTHREAD_MUTEX_RECURSIVE(递归锁等属性;

它的简单用法如下:


    pthread_mutexattr_t attr;
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); // 释放锁

使用注意
同一线程,多次获得同一锁时,会造成死锁,此时应使用PTHREAD_MUTEX_RECURSIVE属性。
三、pthread_rwlock
简介

pthread_rwlockpthread中定义的读写锁,相关函数如下:


__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_destroy(pthread_rwlock_t * ) __DARWIN_ALIAS(pthread_rwlock_destroy);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
const pthread_rwlockattr_t * _Nullable __restrict)
__DARWIN_ALIAS(pthread_rwlock_init);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_rdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_rdlock);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_tryrdlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_tryrdlock);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_trywrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_trywrlock);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_wrlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_wrlock);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlock_unlock(pthread_rwlock_t *) __DARWIN_ALIAS(pthread_rwlock_unlock);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t * __restrict,
int * __restrict);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_init(pthread_rwlockattr_t *);
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int);

pthread_mutex 类似,pthread_rwlock也可以通过宏定义快速初始化:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
也可以通过int pthread_rwlock_init(pthread_rwlock_t * __restrict, const pthread_rwlockattr_t * _Nullable __restrict)初始化,其中pthread_rwlockattr_t是属性对象;
属性对象可以通过int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);初始化。同时可以通过int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int)用来设置读写锁的作用范围,这里需要的int类型有两个宏定义PTHREAD_PROCESS_SHARED:该读写锁可以在多个进程中的线程之间共享。PTHREAD_PROCESS_PRIVATE:仅初始化本读写锁的线程所在的进程内的线程才能够使用该读写锁。
使用注意
由于读写锁的性质,必须要等到所有读锁都释放之后,才能成功申请写锁,这就很容易导致写线程饥饿。
四、NSLock
简介

NSLock是典型的面向对象的锁,遵循Objective-CNSLocking协议接口,该协议定义了lockunlock。此外NSLock类还增加了tryLocklockBeforeDate:方法。tryLock方法尝试获取锁,如果锁不可用返回NOlockBeforeDate:尝试在指定时间内获取锁,如不成功返回NO


    NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1 准备加锁ing...");
[lock lock];
NSLog(@"线程1 锁定成功");
sleep(5);//睡眠5秒
NSLog(@"线程1 准备解锁");
[lock unlock];
NSLog(@"线程1 解锁成功");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2 尝试加锁ing...");
BOOL x = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
if (x) {
NSLog(@"线程2 锁定成功");
[lock unlock];
NSLog(@"线程2 解锁成功");
}else{
NSLog(@"线程2 加锁失败");
}
});

实现原理

NSLock是在内部封装了一个 pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。
使用注意
NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。解锁来自不同线程的锁可能会导致未定义的行为。
在同一线程上两次调用lock方法,会造成死锁,因此在递归中不能使用NSLock。应使用NSRecursiveLock
五、NSRecursiveLock
简介

NSRecursiveLock是面向对象的递归锁,同样遵循Objective-CNSLocking协议接口。该锁可被同一线程多次获取,而不会造成死锁。只是记录获取锁成功的次数,只有调用解锁的次数与锁定次数相同时,锁才会被真正释放,此时才能被其它线程获取。


NSRecursiveLock *rLock = [NSRecursiveLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[rLock lock];
if (value > 0) {
NSLog(@"线程%d", value);
RecursiveBlock(value - 1);
}
[rLock unlock];
};
RecursiveBlock(4);
});

实现原理

NSRecursiveLock递归锁也是通过 pthread_mutex 来实现,与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型为 PTHREAD_MUTEX_RECURSIVE
使用注意
因为在所有锁定调用与解锁调用保持平衡之前,递归锁定不会被释放,所以我们应该仔细衡量使用递归锁潜在性能影响。长时间保持锁可能导致其他线程阻塞,直到递归完成。如果可以,我们应该从代码设计上尽量避免递归锁。
六、NSCondition
简介

NSCondition同样遵循NSLocking协议,NSCondition提供了单独的信号量管理接口。


- (void)wait;//阻塞当前线程直到条件锁发出信号为止,在调用此方法之前必须锁定接收器。
- (BOOL)waitUntilDate:(NSDate *)limit;//阻塞当前线程直到条件锁发出信号或达到指定的时间限制为止。
- (void)signal;//条件锁的信号,唤醒一个等待的线程,可多次调用唤醒多个线程,如没有被锁定的线程,则不起任何作用。为了避免竞争条件锁,应该仅在接收器锁定时调用此方法。
- (void)broadcast;//唤醒全部等待的线程。

实现原理

NSCondition是通过条件变量(condition variable) pthread_cond_t 来实现的,同时封装了一个互斥锁和条件变量。提供了线程阻塞与信号机制。
使用注意
NSCondition是通过条件变量实现,而条件变量必须和一个互斥锁配合, 以防止多个线程同时请求pthread_cond_wait()的竞争条件。所以在调用waitwaitUntilDate方法前,必须在本线程调用lock方法,确保当前线程为锁定状态;
wait函数并不是完全可信的,可能存在虚假唤醒。也就是说wait返回后,并不代表对应的事件一定被触发了,因此,为了保证线程之间的同步关系,使用NSCondtion时往往需要加入一个额外的变量来对非正常的wait返回进行规避。
//等待事件触发的线程  
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];

timeToDoWork--;

// Do real work here.
[cocoaCondition unlock];

//出发事件的线程
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

经笔者测试,当多个线程被阻塞时,通过调用signal进行唤醒操作,线程被唤醒的顺序与wait的调用顺序相关,而与线程的优先级无关。
        NSLog(@"thread1:等待发送1");
[cocoaCondition lock];
[cocoaCondition wait];

NSLog(@"thread1:发送1");
[self.condition unlock];

});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
sleep(1);

NSLog(@"thread2:等待发送2");
[cocoaCondition lock];
[cocoaCondition wait];

NSLog(@"thread2:发送2");
[cocoaCondition unlock];

});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
[cocoaCondition lock];
NSLog(@"thread3:收到数据");
[cocoaCondition signal];
[cocoaCondition unlock];

});

打印结果:
thread1:等待发送1
thread2:等待发送2
thread3:收到数据
thread1:发送1

七、NSConditionLock
简介

NSConditionLock遵循NSLocking协议,可以与用户自定义的条件相关联的互条件锁,是互斥锁的变种。提供了更加直观、方便的条件管理接口,可以更方便的实现生产者-消费者模式。


- (instancetype)initWithCondition:(NSInteger)condition;//初始化一个条件锁,并设置条件;
- (void)lockWhenCondition:(NSInteger)condition;//当条件满足时,获取锁。
- (BOOL)tryLock;//不考虑条件,直接尝试获取锁,成功返回YES,反之为NO。
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//条件满足时尝试锁定,成功返回YES,反之为NO。
- (void)unlockWithCondition:(NSInteger)condition;//解锁并重新设置条件。
- (BOOL)lockBeforeDate:(NSDate *)limit;//在限制期限内获取锁,成功返回YES,反之为NO。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//当条件满足时,在指定期限内获取锁,此方法会阻塞线程,直达获取锁(返回YES)或超时(返回NO)。

NSConditionLock可以很方便的实现线程间的依赖关系:


    id condLock = [[NSConditionLock alloc] initWithCondition:0];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condLock lockWhenCondition:2];
NSLog(@"线程1");
[condLock unlockWithCondition:0];

});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condLock lockWhenCondition:1];
NSLog(@"线程2");
[condLock unlockWithCondition:2];

});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condLock lockWhenCondition:0];
NSLog(@"线程3");
[condLock unlockWithCondition:1];

});

打印结果:
线程3
线程2
线程1

实现原理

NSConditionLock 借助 NSCondition 来实现 内部持有一个NSCondition对象,和一个_condition_value属性,调用lockWhenCondition:时,只有_condition_value条件值相等时,才能获得锁。


八、dispatch_semaphore
简介

dispatch_semaphore信号量,GCD中基于信号控制访问资源的线程数量。当限定的线程数量为一时,就起到了和同步锁相同的效果;信号量主要的函数如下:


dispatch_semaphore_create(long value);//用初始值创建新的计数信号量。注意:value值必须>=0,否则返回NULL。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待(递减)信号量。
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//信号(增加)信号量。

信号量的简单用法如下:


    //value表示最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//线程1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"线程1:启动");
sleep(1);
NSLog(@"线程1:完成");
dispatch_semaphore_signal(semaphore);
});
//线程2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"线程2:启动");
sleep(1);
NSLog(@"线程2:完成");
dispatch_semaphore_signal(semaphore);
});
//线程3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"线程3:启动");
sleep(1);
NSLog(@"线程3:完成");
dispatch_semaphore_signal(semaphore);
});

当value = 3时,打印结果:
线程1:启动
线程2:启动
线程3:启动

线程1:完成
线程2:完成
线程3:完成

当value = 2时,打印结果:
线程1:启动
线程2:启动

线程1:完成
线程2:完成
线程3:启动

线程3:完成

当value = 1时,打印结果:
线程1:启动

线程1:完成
线程2:启动

线程2:完成
线程3:启动

线程3:完成

当信号计数大于0时,每条进来的线程使调用dispatch_semaphore_wait函数使计数减1;直到信号技术为0,阻塞其他线程,直到执行的线程调用dispatch_semaphore_signal函数使级数加1,信号技术大于0,允许阻塞的线程启动;可见,当信号计数控制为1时,可实现同步锁的作用。


实现原理

想要深入的理解dispatch_semaphore,可以参考这篇博客和源码。


使用注意
创建信号量时,传入的value参数必须>=0,否则返回NULL;
timeout参数为dispatch_time_t类型。比较有用的两个宏是DISPATCH_TIME_NOW(表示当前,立即返回超时)和DISPATCH_TIME_FOREVER (表示遥远的未来,一直等待下去)。一般可以直接设置timeout为这两个宏其中的一个,或者自己创建一个dispatch_time_t类型的变量。创建dispatch_time_t类型的变量有两种方法,dispatch_timedispatch_walltime
通过dispatch_semaphore特征我们不难发现, 它不支持递归。
九、OSSpinLock
简介

OSSpinLock是iOS/MacOS自有的自旋锁,其特点是线程在等待取锁时,不会被挂起,而是保持空转,这避免了上下文切换等锁的操作。适用于临界区耗时短的操作,如果等待取锁的时间过长,轮训操作会消耗大量CPU资源。其主要函数和简单适用如下:


typedef int32_t OSSpinLock;
bool OSSpinLockTry( volatile OSSpinLock *__lock );
void OSSpinLockLock( volatile OSSpinLock *__lock );
void OSSpinLockUnlock( volatile OSSpinLock *__lock );

__block OSSpinLock spinLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(& spinLock);
NSLog(@"线程1");
sleep(10);
OSSpinLockUnlock(& spinLock);
NSLog(@"线程1解锁成功");
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
OSSpinLockLock(& spinLock);
NSLog(@"线程2");
OSSpinLockUnlock(& spinLock);
});

因优先级反转的问题,在不同优先级的线程中OSSpinLock已不在安全,详细的介绍可参见这篇博客不再安全的 OSSpinLock,这里不在做过多介绍。


性能对比

这里贴出一张ibireme测试的结果图:





性能对比
总结

以上介绍的几种加锁方式,在原理、用法和性能等方面上各有不同。我们不能单从性能方面评论孰好孰坏。应该根据不同的需求和场景,选取合适的加锁方式。


对性能要求苛刻,且临界区耗时很短,可以考虑自旋锁;
同样对性能要求高,dispatch_semaphorepthread_mutex也是不错的选择;
如果要临界区有大量的读写操作,且读多写少,那么pthread_rwlock或许是不错的选择;
NSLockNSRecursiveLock更加面向对象,用起来或许跟顺手;
NSConditionLock虽然性能不强,但是提供了方便的面向对象的条件管理接口;
在递归中使用pthread_mutex(recursive)NSRecurisiveLcok都不错;
@synchronized虽然性能最差,但是操作简单快捷,如果是低频操作,或者紧急修复,它也是很好的选择;
参考资料

Synchronization


More than you want to know about @synchronized


pthread_mutex_lock.c 源码


深入理解 iOS 开发中的锁


深入理解GCD


pthread的各种同步机制


不再安全的 OSSpinLock








最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台