527 阅读6分钟

前言

在ios开发中,锁在ios内部其实用的很多,但是在外层,个人用的并不多,对于这一块也是比较陌生,如果了解到位,可能在项目中会解决很多以前认为复杂的问题。

锁的性能分析

通过循环十万次加锁解锁获得对应的锁所消耗的时间获得对应的性能如下

截屏2021-08-18 上午10.16.04.png

@synchronized

在GCD中已经见过并且用过@synchronized,这也是最常见的锁。使用

    @synchronized(self) {
    }

如何探索?

汇编+xcrun,通过xcrun看他的结构(命令:xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc main.m),打开查看它的内部结构。

声明变量,然后就是进入函数enter,try Catch只看正常情况,里面是个析构函数,最终代码块结束会调用exit,所以重点只需要看objc_sync_enterobjc_sync_exit,也就是一个入口一个出口。

1629254728339.jpg

跟流程,根据上面的objc_sync_enterobjc_sync_exit这两个方法,下符号断点,找到对应的源码所在的库进而找到源码,就在libobjc.A.dylib.

截屏2021-08-18 上午10.51.35.png

也可以不用clang命令,直接通过汇编查看下一个符号是什么,然后下符号断点即可。

截屏2021-08-18 上午10.55.23.png

换到底层,只是也就是只是做了加锁和解锁。

1629256867825.jpg

1629256899133.jpg

SyncData

既然如此,那么在调用进出函数之前,SyncData做了什么呢?是什么样的数据呢?

内部是一个结构体,第一个参数是有点类似于类里面讲到的,其实也就是个单向链表,这里也就是为什么跟递归关联,第二个是对object的封装,变成无符号的一个长整型,可以方便计算,threadCount满足了多线程的递归,而recursive_mutex_t不可以,他是中对递归锁recursive_mutex_tt封装。 截屏2021-08-18 上午11.23.52.png

id2data方法

前面是获取相关的锁以及data,然后分别判断是否支持线程暂存空间,是的话走data否则cache,这是两种存储的方式。然后开辟内存对齐好的空间并且赋值,最后对本次线程解锁,防止多线程冲突,然后返回result。

截屏2021-08-18 下午2.20.11.png

通过宏生成的lockp和listp,看一下宏定义, sDataLists全局静态变量。 截屏2021-08-18 下午2.23.19.png

但是sDataLists并没有具体的定义,无法知道内部是存在着什么??怎么办?通过lldb调试打印出。 这个数组包含了64条数据。并且传了对象以后,data的数据会发生变化,也就是说data的变化依赖于object。并且存储不是顺序的,验证了这是一张哈希表。

截屏2021-08-18 下午2.31.04.png

如果是第一次进来,data是空,只会走下面, 截屏2021-08-18 下午3.05.57.png

那么有了对象以后,接下来是怎么操作呢?两种逻辑类似,以其中之一讨论。 首先是获取锁的次数,并且有异常判断,然后是判断状态, 加或者减,减到0,那么这个对象其他线程可以使用,意味着可多线程。threadCount记录被多少条线程捕获,lockcount记录在这条线程被锁了多少次。

SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);

    **if** (data) {

        fastCacheOccupied = **YES**;

\


        **if** (data->object == object) {

            // Found a match in fast cache.

            uintptr_t lockCount;

\


            result = data;

            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);

            **if** (result->threadCount <= 0  ||  lockCount <= 0) {

                _objc_fatal("id2data fastcache is buggy");

            }

\


            **switch**(why) {

            **case** ACQUIRE: {

                lockCount++;

                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (**void***)lockCount);

                **break**;

            }

            **case** RELEASE:

                lockCount--;

                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (**void***)lockCount);

                **if** (lockCount == 0) {

                    // remove from fast cache

                    tls_set_direct(SYNC_DATA_DIRECT_KEY, **NULL**);

                    // atomic because may collide with concurrent ACQUIRE

                    OSAtomicDecrement32Barrier(&result->threadCount);

                }

                **break**;

            **case** CHECK:

                // do nothing

                **break**;

            }

\


            **return** result;

        }

    }

注意锁的对象最好不要给局部变量,因为可能存在,局部变量在函数出去以后已经释放了,但是内部管理的是全局变量,用self的话,就可以把所有管理的变量或者对象拉链连起来了。

总体结构总结

截屏2021-08-19 下午3.35.47.png

首先@synchronized是一个哈希结构,由于架构不同,最左边分配给他的大小不同,真机是8个大小,模拟器或者mac是64,可以理解,毕竟在电脑上可以给更多的空间,但是与此同时变消耗了更多时间去操作,所以用的时间在模拟器和真机上是不同的。模拟器上的时间要大于真机。cpu会在闲的时候立马回收,所以不用担心存在不够用的情况。并且内部实现了两种存储方式,tlscache,tls保证了多线程,第一次进来采用头插法标记threadCount等于1,以后每次进来,判断是否同一个对象,如果是,执行++或者--(lock ++ 或者 --),如果不是,则创建(threadCount ++ 或者 -- )。如果把64改成1,给两个对象分别都上锁,那么在第二个对象的data创建出来的时候,他的nextdata就是第一个对象。也就是说他们形成了拉链。

锁的种类

互斥锁

线程之间同时处理任务,互斥锁就是有一个在执行,另外的就不能执行(互斥),并且两者之间是存在顺序关系的。(同步)并且闲时等待。

NSLock:非递归锁,如果在多线程中存在递归函数加锁,那么用这个锁是没有用的,会造成死锁。

recursiveLock:可递归非多线程。如果在多线程中存在递归函数加锁,那么只能加锁解锁当前的线程,不能解决多线程的递归。

@synchronized:可递归可多线程。

NSCondition:生产-消费。场景是每当生产出来,才能消费,但是存在情况当前产量0,我还要消费,消费不了。NSCondition可以在此时发起wait,在每次生产成功以后发起信号量通知,其他还是正常操作,生产加锁解锁,消费加锁解锁,保证每个环节的安全,比一般的加锁多了中间判断流程。

NSConditionLock

截屏2021-08-23 上午11.12.53.png

执行结果可能是321也有可能是231,但因为2是需要等待0.1,所以更多是321.

如果初始化了条件,按照条件执行的走,只有条件是2才会执行,其他都需要等待,如果没有在加锁的时候输入条件,正常执行。

探索 NSConditionLock

断点+汇编调试!!!!

如果想看initWithCondition内部的流程怎走的,光打方法的断点不行,因为他是一个对象方法,[NSConditionLock initWithCondition:]直接定位。通过寄存器打印,第一个是方法接收者,第二个是方法名,第三个是参数。

截屏2021-08-23 上午11.37.24.png

接下来看怎么跳转进哪个方法。看bl跳转,在每个bl的地方打断点再通过寄存器读取,看他底层到底调用了哪些对象的哪些方法,以及用到了哪些参数。

在最后retun的时候,查看当前的寄存器,读取内存第一个保存的地址,就是返回的对象,然后打印这个对象的内存结构,分析他的成员变量结果这里封装了一个NSCondition,并且参数也传了进去。

截屏2021-08-23 上午11.54.46.png

自旋锁

有一个任务在执行,另外的就不停地在等待,不出来不走。

读写锁

多读单写功能实现

写入

通过barrier栅栏函数,实现对队列的顺序写入,不会导致多线程写入冲突,也就是说不同的线程耗时不同,但是如果第一个走不完,后面的也不会走,通过async不会堵塞主线程。 截屏2021-08-23 下午4.05.19.png

读取 通过对队列中同步获取,实现同步读取内容,不会导致因线程过快返回而没有获取到数据。外层只需要在全局队列中添加读取方法,不会堵塞主线程。

截屏2021-08-23 下午4.05.03.png