前言
在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。
概述
锁是多线程开发中最基本的同步工具。开发中常用的锁通常分为以下几种类型:
-
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()
并没有对传入的对象做retains
和releases
; -
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_mutex
有PTHREAD_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_rwlock
是pthread
中定义的读写锁,相关函数如下:
__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-C
的NSLocking
协议接口,该协议定义了lock
和unlock
。此外NSLock
类还增加了tryLock
和 lockBeforeDate:
方法。tryLock
方法尝试获取锁,如果锁不可用返回NO
。lockBeforeDate:
尝试在指定时间内获取锁,如不成功返回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-C
的NSLocking
协议接口。该锁可被同一线程多次获取,而不会造成死锁。只是记录获取锁成功的次数,只有调用解锁的次数与锁定次数相同时,锁才会被真正释放,此时才能被其它线程获取。
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()
的竞争条件。所以在调用wait
或waitUntilDate
方法前,必须在本线程调用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_time
和dispatch_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_semaphore
和pthread_mutex
也是不错的选择; - 如果要临界区有大量的读写操作,且读多写少,那么
pthread_rwlock
或许是不错的选择; -
NSLock
和NSRecursiveLock
更加面向对象,用起来或许跟顺手; -
NSConditionLock
虽然性能不强,但是提供了方便的面向对象的条件管理接口; - 在递归中使用
pthread_mutex(recursive)
和NSRecurisiveLcok
都不错; -
@synchronized
虽然性能最差,但是操作简单快捷,如果是低频操作,或者紧急修复,它也是很好的选择;
网友评论