iOS中的线程同步方案
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
1.OSSpinLock
1> OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源;High-level lock,高级锁,等不到锁时忙等,不会休眠
2> 目前已经不再安全,可能会出现优先级反转问题
3> 如果等待锁的优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
4> 需要导入头文件 #import <libkern/OSAtomic.h>
自旋锁优先级反转:有两个线程,thread1(高优先级)、thread2(低优先级),thread2先进行加锁操作,CPU切换调度,thread1进入,发现已经被锁,进入自旋状态。由于thread1优先级比thread2高出很多,CPU接下来可能一直调度thread1,处于自旋状态,相当于一直执行不到thread2的解锁操作,造成优先级反转现象
1 | // 初始化锁 |
OSSpinLock是性能最高的锁,因为加锁操作耗时较短的话,忙等一会儿直接继续执行了;反之休眠的话唤醒也是需要耗性能的。但是现在已经不再安全了,所以苹果不建议我们再继续使用OSSpinLock了
验证OSSpinLock自旋锁忙等:
Xcode -> Debug Workflow -> Always show Disassembly
step::代码级别一行一行走
stepi:汇编指令一行一行走,简称 si
nexti:汇编指令一行一行走,但是如果遇到函数调用不会进去,会直接跳过
我们通过 si 指令,一行一行走,进入 OSSpinLockLock 函数,继续,进入 _OSSpinLockLockSlow 函数,这个时候要注意了:会一直在一块儿内存地址代码之间重复执行,这种就是典型的while循环,自旋锁,只有锁被放开之后才会往下继续执行
2.os_unfair_lock_lock
1> os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始支持
2> 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等;Low-level lock,低级锁,等不到锁时休眠
3> 需要导入头文件 #import <os/lock.h>
1 | // 初始化 |
验证 os_unfair_lock_lock 非忙等:
os_unfair_lock_lock -> _os_unfair_lock_lock_slow -> __ulock_wait,进入之后,发现断点过着过着、到syscall时直接过去了,直接休眠了,这也说明os_unfair_lock_lock线程等待时并非忙等,而是休眠了
3. pthread_mutex
1> mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
2> 需导入头文件 #import <pthread.h>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/*
* Mutex type attributes
*/
// 普通锁
// 检查错误的
// 递归锁
// 初始化锁的属性
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_trylock(&mutex);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁相关资源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
将属性attr的type由PTHREAD_MUTEX_NORMAL改为 PTHREAD_MUTEX_RECURSIVE,这个锁就变成了递归锁:允许 同一个线程 对 同一把锁 进行 重复加锁
4.pthread_mutex - 递归锁
1 | pthread_mutexattr_t attr; |
5.pthread_mutex - 条件
1 | // 初始化锁 |
6.NSLock、NSRecurisiveLock
1> NSLock 是对mutex普通锁的封装
2> NSRecursiveLock 也是对mutex递归锁的封装,API跟NSLock基本一致1
2
3
4
5
6
7
8
9@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking>
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
NSLock 是OC对象,再从堆栈一行一行找的话不好看锁的执行流程,因为OC对象通过消息机制执行方法,堆栈里面有很多消息机制流程,对汇编不是很熟悉的话不好断点。
那我们就放弃了么,NO,NO,还是可以通过 GUNStep -> GNUstep Base 来参考下
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
源码地址:http://www.gnustep.org/resources/downloads.php
下载之后打开工程,搜索 NSLock.m,找到 initialize 方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35+ (void) initialize
{
static BOOL beenHere = NO;
if (beenHere == NO)
{
beenHere = YES;
/* Initialise attributes for the different types of mutex.
* We do it once, since attributes can be shared between multiple
* mutexes.
* If we had a pthread_mutexattr_t instance for each mutex, we would
* either have to store it as an ivar of our NSLock (or similar), or
* we would potentially leak instances as we couldn't destroy them
* when destroying the NSLock. I don't know if any implementation
* of pthreads actually allocates memory when you call the
* pthread_mutexattr_init function, but they are allowed to do so
* (and deallocate the memory in pthread_mutexattr_destroy).
*/
pthread_mutexattr_init(&attr_normal);
pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
pthread_mutexattr_init(&attr_reporting);
pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
/* To emulate OSX behavior, we need to be able both to detect deadlocks
* (so we can log them), and also hang the thread when one occurs.
* the simple way to do that is to set up a locked mutex we can
* force a deadlock on.
*/
pthread_mutex_init(&deadlock, &attr_normal);
pthread_mutex_lock(&deadlock);
}
}
我们发现,其实就是 pthread_mutex_init(&deadlock, &attr_normal);,所以 NSLock 其实就是对 pthread_mutex 普通锁的封装
1 | // NSRecursiveLock |
7.NSCondition
1> NSCondition是对mutex和cond的封装1
2
3
4
5
6@interface NSCondition : NSObject <NSLocking> {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@end
GNUStep实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_cond_init(&_condition, NULL))
{
DESTROY(self);
}
else if (0 != pthread_mutex_init(&_mutex, &attr_reporting))
{
pthread_cond_destroy(&_condition);
DESTROY(self);
}
}
return self;
}
- (void) signal
{
pthread_cond_signal(&_condition);
}
- (void) wait
{
pthread_cond_wait(&_condition, &_mutex);
}
8.NSConditionLock
1> NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值1
2
3
4
5
6
7
8
9
10@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end
GUNStep:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- (id) init
{
return [self initWithCondition: 0];
}
- (id) initWithCondition: (NSInteger)value
{
if (nil != (self = [super init]))
{
if (nil == (_condition = [NSCondition new]))
{
DESTROY(self);
}
else
{
_condition_value = value;
}
}
return self;
}
9.dispatch_queue(DISPATCH_QUEUE_SERIAL) GCD串行队列
1 | dispatch_queue_t queue = dispatch_queue_create("top.istones.moneyQueue", DISPATCH_QUEUE_SERIAL); |
10.dispatch_semaphore
1> semaphore叫做”信号量”
2> 信号量的初始值,可以用来控制线程并发访问的最大数量
3> 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
1 | // 信号量的初始值 |
11.@synchronized
1> @synchronized是对mutex递归锁的封装
2> 源码查看:objc4中的objc-sync.mm文件
3> @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
1 | @synchronized(obj) { // objc_sync_enter |
注意:效率非常低,苹果已经不推荐使用,Xcode也没有提示了
Xcode断点,发现,@synchronized在大括号进入和退出时分别调用的是 objc_sync_enter 和 objc_sync_exit。在 objc-sync.mm 文件中搜索:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
性能
性能由高到低:
1> os_unfair_lock
2> OSSpinLock
3> dispatch_semaphore
4> pthread_mutex
5> dispatch_queue(DISPATCH_QUEUE_SERIAL)
6> NSLock
7> NSCondition
8> pthread_mutex(recursive)
9> NSRecursiveLock
10> NSConditionLock
11> @synchronized
推荐使用 dispatch_semaphore、pthread_mutex,os_unfair_lock 性能虽然高,但是从 iOS10 才开始支持
什么情况下使用自旋锁比较划算
1> 预计线程等待锁的时间很短
2> 加锁的代码(临界区)经常被调用,但竞争很少发生
3> CPU 资源不紧张
4> 多核处理器
什么情况下使用互斥锁比较划算
1> 预计线程等待锁的时间较长
2> 单核处理器
3> 临界区有IO操作
4> 临界区代码复杂或者循环量大
5> 临界区竞争非常激烈
atomic
1> atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
2> 可以参考源码 objc4 的 objc-accessors.mm
3> 它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全)
提到 atomic,我们想到更多的是 nonatomic,atomic 原子性,在 macOS 中有用,在 iOS 项目中几乎不会用
1 | /** |
打开objc4源码,搜索 objc-accessors.mm 文件,观察 reallySetProperty 和 objc_getProperty 方法:
1 | static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) |
我们可以发现,如果是atomic,就加了一把自旋锁,如果是nonatomic,直接set或返回值
iOS中的读写安全方案
我们先思考如何实现以下场景:
1> 同一时间,只能有1个线程进行写的操作
2> 同一时间,允许有多个线程进行读的操作
3> 同一时间,不允许既有写的操作,又有读的操作
上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作
1 | - (void)test { |
好像简单这样用semphore的话并不能实现安全多读单写,那么怎么实现呢?
iOS中的实现方案有:
1> pthread_rwlock:读写锁
2> dispatch_barrier_async:异步栅栏调用
pthread_rwlock
等待锁的线程会进入休眠1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);
dispatch_barrier_async
1> 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
2> 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
1 | dispatch_queue_t queue = dispatch_queue_create("top.istones.rwQueue", DISPATCH_QUEUE_CONCURRENT); |
注意:读和写一定要放到一个队列里面!