recursive_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
45
46
47
48
49
50
51
52
53
54
@interface ViewController (){
pthread_mutex_t _interceptorsLock;
}

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

pthread_mutexattr_t interattr;
pthread_mutexattr_init (&interattr);
pthread_mutexattr_settype (&interattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_interceptorsLock, &interattr);
pthread_mutexattr_destroy (&interattr);

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test];
});

// 1
dispatch_async(dispatch_get_main_queue(), ^{
[self test2];
});
}

- (void)test {

NSLog(@"lock 1");
pthread_mutex_lock(&_interceptorsLock);

// 2
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispachsync");
});

pthread_mutex_unlock(&_interceptorsLock);
NSLog(@"unlock 1");
}

- (void)test2 {

NSLog(@"call test2");
NSLog(@"lock 2");

pthread_mutex_lock(&_interceptorsLock);

NSLog(@"1122---2");

pthread_mutex_unlock(&_interceptorsLock);
NSLog(@"unlock 2");
}

运行之后,日志卡在这里

1
2
3
2023-07-06 17:18:50.897834+0800 AAAA[82542:2621413] lock 1
2023-07-06 17:18:51.016660+0800 AAAA[82542:2620769] call test2
2023-07-06 17:18:51.016700+0800 AAAA[82542:2620769] lock 2

点击 debug 调试后,发现 死锁了

一开始以为,这个锁是 递归锁, 递归所就是可以重复的加锁,不应该有什么问题。后来问了 chatGPT,也说不会有问题,但它给出了一个重要的信息,就是重复加锁,应该是在同一个线程中

而且 Apple 文档也支出,针对的是同一个线程

梳理下调用流程,是这样的

首先我们在 global_queue 中执行 test 方法,其会在子线程,这时子线程 正常加锁,然后里边执行 dispatch_sync。由于是 dispatch_sync 函数,后边的 解锁代码需要等待 这个函数内的 block 执行完毕。
但是在之前,我们通过 dispatch_asyncmain_queue 中执行了 test2
我们知道 dispatch_async 执行其实是把这个 test2 这个任务放在队尾。也就是说 NSLog(@"dispachsync"); 需要等待 test2 执行完才可以执行。

我们来看 test2 函数。这个函数一开始进来先打印,这个没问题,接下来就开始加锁。虽然这个是递归锁,但是由于这是在不同的线程中,它其实就是一个 互斥锁。但这个锁 在之前执行 test 的时候,已经被加锁了。这里只能等 test 中解锁。这样就死锁了

但这个也不是必现的。如果 main_queueNSLog(@"dispachsync");[self test2] 之前的话,则可以正常执行。

进一步思考,既然 递归锁 是为了避免在同一个线程中重复加锁导致的死锁。那既然是同一个线程,则都是 串行 执行的,也不会有资源竞争。那就可以不加锁。那么问题来了。递归锁存在的意义是什么?

其实仔细想下,还是为了避免多线程资源竞争的问题。
虽然 递归锁 避免同一个线程中多次加锁的死锁问题,但在多线程下,其也是一个 互斥锁,也能处理资源竞争问题。

考虑这样一个代码

1
2
3
4
5
6
7
- (void)method {
[lock lock]; // 递归锁
// some code
// possibly call [self method]
a += 1
[lock unlock];
}

如果不加锁的话,两个线程在调用 method 方法后,a 的值可能不能正常处理。当加了这个递归锁之后,可以保证多线程下 a 可以被正确处理