iOS底层分析-锁(一)

这是我参与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.mcpp文件;找到main函数,对照main的原始方法,我们可以分析出@synchronized的核心内容为红框内代码;

将代码进行格式化之后如下:

很明显catch及其后边处理异常的代码并非我们需要关注的内容,那么我们只需要关注的代码就只剩下:

然后_SYNC_EXIT结构体是一个结构体,可以领出去;_sync_objappDelegateClassName也不用研究,那么最后就只剩下代码:

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_enterobj存在时生成一个SyncData类型的数据:SyncData* data = id2data(obj, ACQUIRE);并且调用了data->mutex.lock();方法进行加锁;
  • objc_sync_enterobj存在时也生成了一个SyncData类型的数据:SyncData* data = id2data(obj, RELEASE);,如果这个数据data存在则调用了data->mutex.tryUnlock();方法进行解锁;

通过比较objc_sync_enterobjc_sync_exit两个方法,我们发现两个方法极其相似,而且两个方法都使用了id2data这个方法来得到SyncData类型的数据,一方加锁,一方解锁,那么我们基本可以判断,synchronized的核心内容就在id2data中;

那么id2data方法做了什么?SyncData又是一个什么样的数据结构呢?请听下回分解,未完待续……

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享