0%

iOS多线程-锁

前面几篇我们探索了iOS使用频率很高的多线程技术GCD,本篇我们探索多线程中一个重要的概念

锁的分类

锁主要分为两大类自旋锁互斥锁

自旋锁

在自旋锁中,线程会反复检查变量是否可用。由于线程这个过程中一致保持执行,所以是一种忙等待。 一旦获取了自旋锁,线程就会一直保持该锁,直到显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合有效的。对于iOS属性的修饰符atomic,自带一把自旋锁

互斥锁

互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(例如全局变量)进行读写的机制,该目的是通过将代码切成一个个临界区而达成。

读写锁

读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.

几种锁的性能对比

我们通过代码打印的方式比较各种锁性能:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
int js_runTimes = 100000;
/** OSSpinLock 性能 */
{
OSSpinLock js_spinlock = OS_SPINLOCK_INIT;
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
OSSpinLockLock(&js_spinlock); //解锁
OSSpinLockUnlock(&js_spinlock);
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"OSSpinLock: %f ms",(js_endTime - js_beginTime)*1000);
}

/** dispatch_semaphore_t 性能 */
{
dispatch_semaphore_t js_sem = dispatch_semaphore_create(1);
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
dispatch_semaphore_wait(js_sem, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(js_sem);
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"dispatch_semaphore_t: %f ms",(js_endTime - js_beginTime)*1000);
}

/** os_unfair_lock_lock 性能 */
{
os_unfair_lock js_unfairlock = OS_UNFAIR_LOCK_INIT;
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
os_unfair_lock_lock(&js_unfairlock);
os_unfair_lock_unlock(&js_unfairlock);
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"os_unfair_lock_lock: %f ms",(js_endTime - js_beginTime)*1000);
}


/** pthread_mutex_t 性能 */
{
pthread_mutex_t js_metext = PTHREAD_MUTEX_INITIALIZER;

double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
pthread_mutex_lock(&js_metext);
pthread_mutex_unlock(&js_metext);
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"pthread_mutex_t: %f ms",(js_endTime - js_beginTime)*1000);
}


/** NSlock 性能 */
{
NSLock *js_lock = [NSLock new];
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
[js_lock lock];
[js_lock unlock];
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"NSlock: %f ms",(js_endTime - js_beginTime)*1000);
}

/** NSCondition 性能 */
{
NSCondition *js_condition = [NSCondition new];
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
[js_condition lock];
[js_condition unlock];
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"NSCondition: %f ms",(js_endTime - js_beginTime)*1000);
}

/** PTHREAD_MUTEX_RECURSIVE 性能 */
{
pthread_mutex_t js_metext_recurive;
pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&js_metext_recurive, &attr);

double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
pthread_mutex_lock(&js_metext_recurive);
pthread_mutex_unlock(&js_metext_recurive);
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(js_endTime - js_beginTime)*1000);
}

/** NSRecursiveLock 性能 */
{
NSRecursiveLock *js_recursiveLock = [NSRecursiveLock new];
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
[js_recursiveLock lock];
[js_recursiveLock unlock];
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"NSRecursiveLock: %f ms",(js_endTime - js_beginTime)*1000);
}


/** NSConditionLock 性能 */
{
NSConditionLock *js_conditionLock = [NSConditionLock new];
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
[js_conditionLock lock];
[js_conditionLock unlock];
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"NSConditionLock: %f ms",(js_endTime - js_beginTime)*1000);
}

/** @synchronized 性能 */
{
double_t js_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < js_runTimes; i++) {
@synchronized(self) {}
}
double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
JSLog(@"@synchronized: %f ms",(js_endTime - js_beginTime)*1000);
}

在iPhone 12pro模拟器打印的结果为:

模拟器锁性能

在iPhone12 mini真机的结果如下:

12mini锁的性能

可以看到模拟器上@synchronized锁性能是比较差的,但12系列(xr经过测试并没提高)手机的性能有很大提升,我们项目中会比较常见,我们就从@synchronized开始探索。

@synchronized

xcrun分析

我们在main.m文件里写一个锁:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
@synchronized (appDelegateClassName) {

}
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

使用xcrun命令将其编译成.cpp文件

xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc main.m

main.cpp文件最下方找到main函数的实习,定位到@synchronized代码块

xcrunsyn

将代码排版之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ 
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
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);
} 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);}
}

加锁成功的情况我们只需要关注try代码块及以上的代码。经过我们的简化后的代码:

1
2
3
id _sync_obj = (id)appDelegateClassName; 
objc_sync_enter(_sync_obj);
objc_sync_exit(sync_exit);

可以看到主要就是执行了两个函数objc_sync_enterobjc_sync_exit

libobjc源码分析

通过打符号断点objc_sync_enter,我们可以知道objc_sync_enterlibobjc源码中。

libobjc_enter

接下来我们在libobjc源码中全局搜索objc_sync_enter:

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;
}

如果传入objnil,会什么也不做。主要看if代码块的代码,可以看到加锁是通过data->mutex.lock(),也就是SyncData的实例,所以我们先探究一下SyncData类型的结构:

SyncData
1
2
3
4
5
6
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
  • nextData:链表结构下一个节点
  • object:参照关联对象的结构,哈希表
  • threadCount:使用block的线程数量
  • mutex:递归锁(单用多线程会出现问题)

在初始化SyncData实例的时候使用的是id2data函数,我们接下来探索这个函数。

id2data函数

id2data函数有150+行,我们先隐藏代码块,总览一下结构:

id2data

函数的最开始两行有两个宏,它们的定义如下:

1
2
3
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

sDataLists是一个静态哈希表结构,我们使用lldb查看它的数据结构:

sData

经过断点调试,第一次进来执行的代码是:

1
2
3
4
5
6
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;//头插法 添加近链表
*listp = result;

这里有个细节就是listp使用的头插法将新的元素添加到链表,它的可递归性的实现依赖了这种数据结构的使用。

小结

  • synchronized的数据结构是哈希表,采用的拉链法处理哈希冲突
  • sDataLists arrary key是和对象相关的,拉链链表里的元素是同一个对象的锁。
  • objc_sync_enter和objc_sync_exit是对称的,它是一把递归锁。
  • 会有两种存储结构:tls和catch
  • 第一次访问syncData采用的是头插法链表结构 标记threadCount = 1
  • 后续访问,会判断是不是同一个对象,同一对象lockcount++,不是同一个对象threadCount++1。
  • synchronized是一种可重入、递归的多线程锁,原因
    • tls保障 threadCount 可以有多个线程对这个锁对象加锁
    • lock++会记录总共锁了多少次。

NSLock和NSRecursiveLock使用

我们以一个实例分析这两种锁的区别:

1
2
3
4
5
6
7
8
9
10
11
12
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}

上面这个代码有多线程冲突的问题,打印的结果无序,不是我们想要的结果

没有锁无序

使用NSLock解决问题

使用NSLock解决的方法其实很简单,就是在我们调用方法testMethod前后加锁。

nslock

我们再看打印结果就正常了。

nslock打印结果

NSLock适用的是在最外层加锁,如果我们能写的代码只能在testMethod操作,这个时候加NSLock就不会正常工作了。

nslock加在业务代码

使用NSRecursiveLock

上面我们知道NSLocktestMethod无法解决问题,我们尝试用NSRecursiveLock解决。

nsrecrusiveLock

发现NSRecursiveLock并不能解决问题,而且还会偶现崩溃。NSRecursiveLock是一把递归锁,但是它并不支持多线程递归。

使用@synchronized

sychorsize解决问题

使用@synchronized解决了业务代码里的问题,说明@synchronized是一把支持多线程的递归锁。

NSCondition

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要 为了当检测条件时保护数据源,执行条件引发的任务;线程检查器 主要是根据条件决定是否继续运行线程,即线程是否被阻塞。它主要有四个方法

  • [condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
  • [condition unlock];//lock 同时使用
  • [condition wait];//让当前线程处于等待状态
  • [condition signal];//CPU发信号告诉线程不用在等待,可以继续执行

它的一个应用场景之一就是生产者-消费者模型。也就是通过多线程进行生产和销售产品,当产品数量为0的时候就只能等待,具体代码如下:

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
- (void)js_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self js_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self js_consumer];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self js_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self js_producer];
});
}
}
- (void)js_producer{
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 信号
[_testCondition unlock];
}
- (void)js_consumer{
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}

Foundation源码看锁的封装

我们在swift-corelibs-foundation源码中探索。

通过源码我们看到,NSLock等锁都实现了一个协议就是NSLocking

1
2
3
4
public protocol NSLocking {
func lock()
func unlock()
}

它们都是对pthread的封装

锁的源码

NSRecursiveLock,也类似,它和NSLock的区别是pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))

nsrecursivelock源码

NSConditionLock

  • NSConditionLock 是锁,一旦一个线程获得锁,其他线程一定等待
  • lock函数:表示 对象期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
  • [xx lockWhenCondition:A条件]方法:表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
  • [xxx unlockWithCondition:A条件]; 表示释放锁,同时把内部的condition设置为A条件
  • return = [xxx lockWhenCondition:A条件 beforeDate:A时间]; 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理。

栅栏函数实现读写锁

读写锁主要要实现以下功能:

  • 多读单写功能。
  • 写入和写入互斥。
  • 读和写入互斥。
  • 写入不能阻塞主线程任务执行。
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
@interface ViewController ()

@property (nonatomic, strong) dispatch_queue_t js_currentQueue;
@property (nonatomic, strong) NSMutableDictionary *mDict;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.js_currentQueue = dispatch_queue_create("jscurrent", DISPATCH_QUEUE_CONCURRENT);
self.mDict = [[NSMutableDictionary alloc] init];
[self js_safeSetter:@"123" time:10];
[self js_safeSetter:@"456" time:5];
[self js_safeSetter:@"789" time:3];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"读取,name = %@ thread---%@",[self js_safeGetter],[NSThread currentThread]);
});
}
}

- (void)js_safeSetter:(NSString *)name time:(int)time{
dispatch_barrier_async(self.js_currentQueue, ^{
sleep(time);
[self.mDict setValue:name forKey:@"name"];
NSLog(@"写入,name = %@ thread---%@",name,[NSThread currentThread]);
});

}

- (NSString *)js_safeGetter{
__block NSString *result;
dispatch_sync(self.js_currentQueue, ^{
result = self.mDict[@"name"];
});
return result;
}
@end

程序运行我们就开始点击屏幕(读操作),最后看打印结果:

读写锁