这是我参与8月更文挑战的第22天,活动详情查看: 8月更文挑战
锁的性能分析
为了测试锁的性能,我们将所有的锁循环进行开锁解锁
操作100000
次,统计其消耗的时间如下:
* 测试设备为iPhone 12
锁的性能,由高到低依次是:
- OSSpinLock(已废弃)
- os_unfair_lock
- pthread_mutex_t
- dispatch_semaphore_t
- NSCondition
- NSlock
- NSRecursiveLock
- pthread_mutex_t
- synchronized
- NSConditionLock
不同设备,真机与模拟器等测试数据可能有差异;
在iOS开发中,我们经常使用synchronized
,所以我们先分析一下synchronized
的原理;
synchronized的原理分析
synchronized实现分析
在分析synchronized
的原理之前,我们先来看一段模拟售票系统的代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self startSaleTicket];
}
- (void)startSaleTicket{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
复制代码
我们运行这段测试代码,看看打印结果:
通过打印结果我们发现,余票数量发生了混乱,这样就会造成数据不安全的问题。那么如何解决这个问题呢?我们可以通过加锁的方式来保证同一时刻只有一个线程在访问对象,这样就能够保证数据的安全性,那么余票数量也能够正确显示了;
我们使用@synchronized
来对售票操作进行加锁,代码修改查看运行结果:
可以看到,票数能改正确的显示了;
@synchronized
具备一下效果:
- 加锁的效果;
- 递归可重入;(可以在
@synchronized
里边继续添加@synchronized
,锁上加锁,递归锁)
可能你已经注意到了,我们在使用@synchronized
的时候,给它传递了一个参数self
,那么这个参数有什么用呢?我们能不能传nil
呢?@synchronized
到底做了什么呢?它究竟是个什么结构呢?
那么我们从哪里作为入口研究呢?根据我们之前经验,可以通过汇编
或者clang
的方式来进行分析;
我们先通过clang
的方式来看一下cpp
文件,为了不被多余的代码侵扰,我们在main.m
文件中使用一下@synchronized
:
然后,我们生成main.m
的cpp
文件;找到main
函数,对照main
的原始方法,我们可以分析出@synchronized
的核心内容为红框内代码;
将代码进行格式化之后如下:
很明显catch
及其后边处理异常的代码并非我们需要关注的内容,那么我们只需要关注的代码就只剩下:
然后_SYNC_EXIT
结构体是一个结构体,可以领出去;_sync_obj
是appDelegateClassName
也不用研究,那么最后就只剩下代码:
objc_sync_enter(_sync_obj);
_sync_exit(_sync_obj);
复制代码
由于_sync_exit
最终会调用结构体_SYNC_EXIT
的析构函数~_SYNC_EXIT()
也就是会调用objc_sync_exit(sync_exit);
;所以最终@synchronized
其实就相当于以下两行代码:
objc_sync_enter(_sync_obj);
objc_sync_exit(_sync_obj);
复制代码
也可以通过在
@synchronized
处打断点,然后查看汇编的方式进行定位objc_sync_enter
;
接下里,我们在工程中添加符号断点objc_sync_enter
,然后运行项目:
发现objc_sync_enter
来自于libobjc.A.dylib
,也就是objc
的源码;前往下载(这里我们以818版本的源码进行分析)
知道了objc_sync_enter
方法的出处,我们就能过以此方法为入口,对@synchronized
进行分析,在objc
源码中,我们定位到objc_sync_enter
实现如下:
在objc_sync_enter
方法的实现中,我们可以看到如果obj
不存在,那么就会执行else
分支,进而执行objc_sync_nil()
方法,其实也就是@synchronized(nil)
参数传nil
的时候;
根据注释信息// @synchronized(nil) does nothing
我们知道,如果参数传nil
,则什么事情都不做;
那么objc_sync_nil()
方法的实现是什么呢?点击发现会跳转到此处:
那么BREAKPOINT_FUNCTION
是什么呢?我们搜索发现其是一个宏定义的封装:
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
复制代码
根据宏定义,我们可以知道,最终代码:
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
复制代码
其实就是:
void objc_sync_nil() {
}
复制代码
也就是说,objc_sync_nil()
什么都没做;
这里就解决了我们的第一个疑问-参数能否传nil
:
@synchronized
可以传nil
,但是传nil
将会什么都不做;
除了objc_sync_enter
方法,我们知道synchronized
还有一个objc_sync_exit
方法,我们在源码中定位此方法:
objc_sync_enter
中obj
存在时生成一个SyncData
类型的数据:SyncData* data = id2data(obj, ACQUIRE);
并且调用了data->mutex.lock();
方法进行加锁;objc_sync_enter
中obj
存在时也生成了一个SyncData
类型的数据:SyncData* data = id2data(obj, RELEASE);
,如果这个数据data
存在则调用了data->mutex.tryUnlock();
方法进行解锁;
通过比较objc_sync_enter
和objc_sync_exit
两个方法,我们发现两个方法极其相似,而且两个方法都使用了id2data
这个方法来得到SyncData
类型的数据,一方加锁,一方解锁,那么我们基本可以判断,synchronized
的核心内容就在id2data
中;
那么id2data
方法做了什么?SyncData
又是一个什么样的数据结构呢?请听下回分解,未完待续……