iOS中的线程同步方案(锁)、读写安全方案

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
2
3
4
5
6
7
// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

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
2
3
4
5
6
7
8
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);

验证 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
*/
// 普通锁
#define PTHREAD_MUTEX_NORMAL 0
// 检查错误的
#define PTHREAD_MUTEX_ERRORCHECK 1
// 递归锁
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

// 初始化锁的属性
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
2
3
4
5
6
7
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)

// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

5.pthread_mutex - 条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化锁
pthread_mutex_t mutex;
// NULL代表使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&condition, &mutex);
// 激活一个等待该条件的线程
pthread_cond_signal(&condition);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&condition);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);

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
2
3
4
5
6
7
8
9
10
11
12
// NSRecursiveLock
- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_mutex_init(&_mutex, &attr_recursive))
{
DESTROY(self);
}
}
return self;
}

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
2
3
4
dispatch_queue_t queue = dispatch_queue_create("top.istones.moneyQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.moneyQueue, ^{
// 任务
});

10.dispatch_semaphore

1> semaphore叫做”信号量”
2> 信号量的初始值,可以用来控制线程并发访问的最大数量
3> 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

1
2
3
4
5
6
7
8
9
// 信号量的初始值
int value = 1;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信号量的值 <= 0,当前线程进入休眠等待(直到信号量的值 > 0)
// 如果信号量的值 > 0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信号量的值加1
dispatch_semaphore_signal(semaphore);

11.@synchronized

1> @synchronized是对mutex递归锁的封装
2> 源码查看:objc4中的objc-sync.mm文件
3> @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

1
2
3
@synchronized(obj) { // objc_sync_enter
// 任务
} // objc_sync_exit

注意:效率非常低,苹果已经不推荐使用,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> 可以参考源码 objc4objc-accessors.mm
3> 它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全)

提到 atomic,我们想到更多的是 nonatomic,atomic 原子性,在 macOS 中有用,在 iOS 项目中几乎不会用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
nonatomic 和 atomic
atom:原子
atomic:原子性

给属性加上atomic属性修饰,可以保证属性的setter和getter都是原子性操作,也就是保证setter和getter内部都是线程同步的,相当于下面:

- (void)setName:(NSString *)name {
// 加锁
_name = name;
// 加锁
}

- (NSString *)name {
// 加锁
// 取值
// 解锁
// 返回
return _name;
}
*/

打开objc4源码,搜索 objc-accessors.mm 文件,观察 reallySetProperty 和 objc_getProperty 方法:

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
45
46
47
48
49
50
51
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}

if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}

objc_release(oldValue);
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}

// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;

// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();

// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}

我们可以发现,如果是atomic,就加了一把自旋锁,如果是nonatomic,直接set或返回值


iOS中的读写安全方案

我们先思考如何实现以下场景:
1> 同一时间,只能有1个线程进行写的操作
2> 同一时间,允许有多个线程进行读的操作
3> 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作

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
- (void)test {
for (NSInteger i = 0; i < 10; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];
}
}

#pragma mark - private methods
/**
如果读也加上 semaphore 的话,确实可以保证读写都安全,同时只能读或者写,但是没必要。iOS中读写方案一般设计多读单写,读不涉及资源抢占,可以同时进行。

但是,这样依然存在问题,虽然可以多读单写。但是写入加锁了,读却没有加锁,读的同时依然可以写,这个依然不可控,依然不安全,不能保证读的同时没有线程在进行写的操作。
*/
- (void)read {
// dispatch_semaphore_wait(self.semphore, DISPATCH_TIME_FOREVER);

NSLog(@"%s", __func__);

// dispatch_semaphore_signal(self.semphore);
}

- (void)write {
dispatch_semaphore_wait(self.semphore, DISPATCH_TIME_FOREVER);

NSLog(@"%s", __func__);

dispatch_semaphore_signal(self.semphore);
}

好像简单这样用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
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("top.istones.rwQueue", DISPATCH_QUEUE_CONCURRENT);

// 读
dispatch_async(queue, ^{

});

// 写
dispatch_barrier_async(queue, ^{

});

注意:读和写一定要放到一个队列里面!

本文demo
objc4
GNUstep Base