多线程中锁的应用

  在多线程运用中,我们经常会遇到资源竞争问题。比如多个线程同时往一个文件写入,这种情况是不被允许的,会造成安全隐患。正常的做法是一个线程写完后,下一个线程才能写入。而在多线程中,这一项技术叫做线程同步技术。

  那么在iOS中,有哪些方案可以使用?

  • pthread_mutex
  • NSLock、NSRecursiveLock
  • NSCondition、NSConditionLock

  • os_unfair_lock(替换被废弃的OSSpinLock,iOS 10+)

  • @synchronized
  • dispatch_semaphore
  • 串行队列

0x01 pthread_mutex

  也叫互斥锁,以休眠来等待锁被解开。以经典的“存取钱”为例,我们看下如何使用pthread_mutex控制线程同步问题。

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
static pthread_mutex_t g_mutex;

- (void)drawMoney {
pthread_mutex_lock(&g_mutex);
NSLog(@"取钱了...");
sleep(2);
pthread_mutex_unlock(&g_mutex);
}

- (void)saveMoney {
pthread_mutex_lock(&g_mutex);
NSLog(@"存钱了...");
sleep(2);
pthread_mutex_unlock(&g_mutex);
}

- (void)initLock {
// 初始化锁的属性
pthread_mutexattr_t attri;
pthread_mutexattr_init(&attri);
pthread_mutexattr_settype(&attri, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attri);

// 销毁锁属性
pthread_mutexattr_destroy(&attri);

g_mutex = mutex;
}

- (void)viewDidLoad {
[super viewDidLoad];

[self initLock];

for (NSInteger i = 0; i < 50; i++) {
[self performSelectorInBackground:@selector(drawMoney) withObject:nil];
[self performSelectorInBackground:@selector(saveMoney) withObject:nil];
}
}

- (void)dealloc {
pthread_mutex_destroy(&g_mutex);
}

  需要注意的是,使用pthread_mutex需要手动进行销毁。

  如下代码,我们进行递归调用的时候会发生什么?

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
- (void)recursiveTest {
pthread_mutex_lock(&g_mutex);
NSLog(@"递归测试...");
[self recursiveTest];
pthread_mutex_unlock(&g_mutex);
}

- (void)initLock {
// 初始化锁的属性
pthread_mutexattr_t attri;
pthread_mutexattr_init(&attri);
pthread_mutexattr_settype(&attri, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attri);

// 销毁锁属性
pthread_mutexattr_destroy(&attri);

g_mutex = mutex;
}

- (void)viewDidLoad {
[super viewDidLoad];

[self initLock];

for (NSInteger i = 0; i < 50; i++) {
[self performSelectorInBackground:@selector(recursiveTest) withObject:nil];
}
}

  由于递归调用,第一次进入recursiveTest的时候调用pthread_mutex_lock加锁,然后在临界区(lock与unlock之间的代码称为临界区)里又调用了自己,然而之前锁已经加上了,所以整理等待解锁,但是等待解锁了就不能继续往下执行,就造成不能执行到pthread_mutex_unlock解锁,这样就导致锁一直不能放开,所以造成了死锁。

  解决这样的问题,我们可以使用递归锁。初始化属性的时候把PTHREAD_MUTEX_NORMAL改为PTHREAD_MUTEX_RECURSIVE

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
- (void)initLock {
// 初始化锁的属性
pthread_mutexattr_t attri;
pthread_mutexattr_init(&attri);
pthread_mutexattr_settype(&attri, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attri);

// 销毁锁属性
pthread_mutexattr_destroy(&attri);

g_mutex = mutex;
}

- (void)recursiveTest {
pthread_mutex_lock(&g_mutex);
NSLog(@"递归测试...");
[self recursiveTest];
pthread_mutex_unlock(&g_mutex);
}

- (void)viewDidLoad {
[super viewDidLoad];

[self initLock];

for (NSInteger i = 0; i < 50; i++) {
[self performSelectorInBackground:@selector(recursiveTest) withObject:nil];
}
}

  我们有时需要加锁的前提是满足某些条件,比如现在有这么一个需求,同时依次调用三个接口,调用完毕后合并这三个接口得到的数据,我们可以实现为

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
static pthread_mutex_t g_mutex;
static pthread_cond_t g_cond;

- (void)initLock {
// 初始化锁的属性
pthread_mutexattr_t attri;
pthread_mutexattr_init(&attri);
pthread_mutexattr_settype(&attri, PTHREAD_MUTEX_DEFAULT);
// 初始化锁条件
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attri);

// 销毁锁属性
pthread_mutexattr_destroy(&attri);

g_mutex = mutex;
g_cond = cond;
}

- (void)loadNetworkData {
pthread_mutex_lock(&g_mutex);
NSLog(@"请求数据...");
sleep(2);
pthread_cond_signal(&g_cond);
pthread_mutex_unlock(&g_mutex);
}

- (void)mergeData {
pthread_cond_wait(&g_cond, &g_mutex);
NSLog(@"合并数据...");
}

- (void)viewDidLoad {
[super viewDidLoad];

[self initLock];
[self performSelectorInBackground:@selector(mergeData) withObject:nil];
for (NSInteger i = 0; i < 3; i++) {
[self performSelectorInBackground:@selector(loadNetworkData) withObject:nil];
}
}

- (void)dealloc {
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_cond);
}

0x02 NSLock、NSRecursiveLock、NSCondition、NSConditionLock

  这几个Lock都是基于mutex的封装。这里只对NSConditionLock做代码示例,其他的api已经够简单明了了,所以不做详细示例。

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
- (void)initLock {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}

- (void)test1 {
[self.conditionLock lockWhenCondition:1];
NSLog(@"测试1...");
sleep(2);
[self.conditionLock unlockWithCondition:2];
}

- (void)test2 {
[self.conditionLock lockWhenCondition:2];
NSLog(@"测试2...");
sleep(2);
[self.conditionLock unlockWithCondition:3];
}

- (void)test3 {
[self.conditionLock lockWhenCondition:3];
NSLog(@"测试3...");
sleep(2);
[self.conditionLock unlock];
}

- (void)viewDidLoad {
[super viewDidLoad];

[self initLock];

[self performSelectorInBackground:@selector(test3) withObject:nil];
[self performSelectorInBackground:@selector(test2) withObject:nil];
[self performSelectorInBackground:@selector(test1) withObject:nil];
}

// 运行结果
2018-04-06 14:45:54.648160+0800 testData[53895:31297465] 测试1...
2018-04-06 14:45:56.654220+0800 testData[53895:31297464] 测试2...
2018-04-06 14:45:58.731368+0800 testData[53895:31297463] 测试3...

  NSConditionLock可以设置多任务间的依赖关系。

0x03 os_unfair_lock

  作为替代OSSpinLock的方案,曾一度以为也是自旋锁,但是查看汇编后调用了睡眠函数。

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)viewDidLoad {
[super viewDidLoad];

[self initLock];

for (NSInteger i = 0; i < 50; i++) {
[self performSelectorInBackground:@selector(drawMoney) withObject:nil];
[self performSelectorInBackground:@selector(saveMoney) withObject:nil];
}
}

- (void)drawMoney {
os_unfair_lock_lock(&_lock);
NSLog(@"取钱了...");
sleep(2);
os_unfair_lock_unlock(&_lock);
}

- (void)saveMoney {
os_unfair_lock_lock(&_lock);
NSLog(@"存钱了...");
sleep(2);
os_unfair_lock_unlock(&_lock);
}

- (void)initLock {
self.lock = OS_UNFAIR_LOCK_INIT;
}

  查看汇编

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
libsystem_platform.dylib`os_unfair_lock_lock:
-> 0x108edd616 <+0>: movl %gs:0x18, %esi
0x108edd61e <+8>: xorl %eax, %eax
0x108edd620 <+10>: lock
0x108edd621 <+11>: cmpxchgl %esi, (%rdi)
0x108edd624 <+14>: jne 0x108edd627 ; <+17>
0x108edd626 <+16>: retq
0x108edd627 <+17>: xorl %edx, %edx
; 调用_os_unfair_lock_lock_slow
0x108edd629 <+19>: jmp 0x108edd62e ; _os_unfair_lock_lock_slow


libsystem_platform.dylib`_os_unfair_lock_lock_slow:
.......
; 调用__ulock_wait
0x108edd6b5 <+135>: callq 0x108edf3b8 ; symbol stub for: __ulock_wait

; 进入睡眠等待
libsystem_kernel.dylib`__ulock_wait:
-> 0x108eb6158 <+0>: movl $0x2000203, %eax ; imm = 0x2000203
0x108eb615d <+5>: movq %rcx, %r10
0x108eb6160 <+8>: syscall
0x108eb6162 <+10>: jae 0x108eb616c ; <+20>
0x108eb6164 <+12>: movq %rax, %rdi
0x108eb6167 <+15>: jmp 0x108eacb00 ; cerror_nocancel
0x108eb616c <+20>: retq
0x108eb616d <+21>: nop
0x108eb616e <+22>: nop
0x108eb616f <+23>: nop

0x04 @synchronized

  @synchronized也是对mutex的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)drawMoney {
@synchronized(self) {
NSLog(@"取钱了...");
sleep(2);
}
}

- (void)saveMoney {
@synchronized(self) {
NSLog(@"存钱了...");
sleep(2);
}
}

- (void)viewDidLoad {
[super viewDidLoad];

for (NSInteger i = 0; i < 50; i++) {
[self performSelectorInBackground:@selector(drawMoney) withObject:nil];
[self performSelectorInBackground:@selector(saveMoney) withObject:nil];
}

}

  使用clang转换代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m

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
static void _I_ViewController_drawMoney(ViewController * self, SEL _cmd) {
{
id _rethrow = 0;
id _sync_obj = (id)self;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_bc9092_mi_0);
sleep(2);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}

}


static void _I_ViewController_saveMoney(ViewController * self, SEL _cmd) {
{ id _rethrow = 0; id _sync_obj = (id)self; objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);

NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_0_j9qb4d4fn9_wnf3fp9q40ssw1_56_T_ViewController_bc9092_mi_1);
sleep(2);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}

}

  转换后的代码,我们可以看到@synchronized内部其实调用了objc_sync_enterobjc_sync_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

  为什么说是对mutex的封装,我们看下SyncData的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;

using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;

class recursive_mutex_tt : nocopy_t {
pthread_mutex_t mLock;
... ...
}

  我们定义属性的时候,对其原子性经常定义为nonatomic,因为性能等原因很少用到atomic。我们知道如果对OC对象使用atomic,会自动在其getter\setter方法里加锁。我们定义属性如下

1
@property (atomic, strong) NSArray *datas;

  我们汇编查看其setter方法,内部调用了objc_setProperty_atomic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
testData`-[ViewController setDatas:]:
0x10169fcf0 <+0>: pushq %rbp
0x10169fcf1 <+1>: movq %rsp, %rbp
0x10169fcf4 <+4>: subq $0x20, %rsp
0x10169fcf8 <+8>: movq %rdi, -0x8(%rbp)
0x10169fcfc <+12>: movq %rsi, -0x10(%rbp)
0x10169fd00 <+16>: movq %rdx, -0x18(%rbp)
-> 0x10169fd04 <+20>: movq -0x10(%rbp), %rsi
0x10169fd08 <+24>: movq -0x8(%rbp), %rdx
0x10169fd0c <+28>: movq 0x8445(%rip), %rcx ; ViewController._datas
0x10169fd13 <+35>: movq -0x18(%rbp), %rdi
0x10169fd17 <+39>: movq %rdi, -0x20(%rbp)
0x10169fd1b <+43>: movq %rdx, %rdi
0x10169fd1e <+46>: movq -0x20(%rbp), %rdx
0x10169fd22 <+50>: callq 0x1016a2e9a ; symbol stub for: objc_setProperty_atomic
0x10169fd27 <+55>: addq $0x20, %rsp
0x10169fd2b <+59>: popq %rbp
0x10169fd2c <+60>: retq

  objc_setProperty_atomic内部使用了os_unfair_lock加锁

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
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

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 {// 如果atomic为YES
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}

objc_release(oldValue);
}

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
os_unfair_lock mLock;
... ...
}

  对于像NSMutableArrayNSMutableDictionary,使用atomic虽然对其setter\getter方法进行了加锁,但是在多线程环境下使用addObject:removeObjectAtIndex:这类对对象内部数据操作的方法,其实还是不安全的。也就是说加锁仅限于读取对象(NSArray *array = self.datas;)和修改对象(self.datas = @[].mutableCopy),但这个对象其内部的方法操作是不安全的。

0x05 dispatch_semaphore和串行队列

  在多线程篇里已经提过,不重复介绍。