iOS-底层原理-24-锁(上)

902 阅读6分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

1.锁的介绍

1.1 锁的性能

开发中使用多线程,就会有线程安全问题,比如在并发队列的异步函数中对数据的读写操作,不加锁就会产生data race(数据竞争) 的情况。我们之前源码探究中也发现关于数据的读写增删都有加锁的操作。我们在实际项目中检测data race的情况可以在设置中选择 Thread Sanitizer进行检测。 image.png
比较常用到锁,比如@synchroizedNSLockdispatch_semaphore等,下图是YY大神的一张锁的性能图 image.png 可以看到其中@synchorized耗时最多,我们自己用代码模拟各个锁耗时,运行下在模拟器iPhone12版本14.5情况下

image.png

用真机上运行iPhone11版本14.5@synchorized明显耗时减少,说明在真机上苹果进行了优化。

image.png

1.2 锁的归类

  • 自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释 放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。常见的自旋锁有

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

    • NSLock
    • pthread_mutex
    • @synchronized
  • 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行

    • NSConditionLock
    • NSCondition
  • 递归锁:就是同一线程加锁N次不引起死锁

    • NSRecursiveLock
    • pthread_mutex(recursive)
  • 信号量(semphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥

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

其实基本的锁就包括了三类 自旋锁 互斥锁 读写锁,其他的比如条件锁递归锁信号量都是上层的封装和实现!

2. @synchorized分析

2.1 分析步骤

@synchrized是我们日常使用开发较高的一个互斥递归锁,比如我们用它定义单例等。我们用Clang编译下看下底层实现

image.png 简化下

image.png

主要是objc_sync_enterobjc_sync_exit

  • 使用汇编验证下

image.png

2.2 objc_sync_enter&objc_sync_exit分析

objc_sync_enter的源码 image.png

判断obj是否为nil,为nil则什么也不做;不为nil,通过id2data获取SyncData,之后对data进行加锁处理。
objc_sync_exit源码: image.png 退出和进入类似,默认int = 0,判断obj是否为nil,为nil则什么也不做;不为nil,通过id2data获取SyncData,判断是否存在锁的数据,不存在则进行result为错误。存在进行尝试解锁,解锁不成功返回错误。

2.2.1 SyncData分析

syncData是一个结构体,包含了:哈希链表,对object封装,使用锁的线程数量则表示可以多线程,递归的属性。

image.png

SyncCache也是一个结构体,包含线程的创建数量,使用数量,当前线程的信息SyncCacheItem是包含锁的数据以及线程锁的次数

image.pngid2datalistp image.png image.png 它的结构就是哈希拉链的形式。

未命名文件-9.jpg 看下list: image.png 这里就是解释了为什么在模拟器上@synchorized的效率比真机要慢,因为在真机上又来储存锁的list大小只有8而模拟器有64个,因此更耗时。真机8个是否够用?用完会进行释放的,因此不必担心,不够的话应该会进行等待

2.2.2 id2data分析

image.png

整体上主要分为3块
1.首先看是否支持tls(线程局部存储)
2.其次看线程的缓存,进行缓存中的
3.最后没有的话表示第一次进入。

我们用代码跟一波流程

image.png 主线程中递归加锁

image.png

  • 第一次进来走SUPPORT_DIRECT_THREAD_KEYS当前线程的SyncDataNull,因为是第一次没有存储对应的锁的信息。

image.png 获取线程的SyncCache也是NULL,同理第一次没有缓存对应的锁的信息。

image.png

进入第一次的操作初始化SyncData,并赋值threadCount1object以及listp哈希链表,这里采用了头插法,标记 thracount = 1。

image.png 保存SyncData到当前线程,并保存lockcount =1

  • 第二次SyncData是储存在当前的tls中走下面流程

image.png 因为是objc_sync_enter所以whyACQUIRE,锁的数量+1,存储锁的数量到当前线程。第三次和第二次流程一样。

  • objc_sync_exit类似 image.png
  • 多线程异步情况

image.png

  • 多线程异步递归情况

image.png

  • 同步递归情况

image.png

id2data中关于SyncData有3种情况:

  1. 第一次进来,没有锁,初始化SyncData,并赋值threadCount1,存储lockCounttls
  2. 第二次进来,支持tls,获取tls中的存储的SyncData,进行lockCount++操作,存储锁的数量到当前线程。
  3. 第二次进来,不支持tls,则使用cache获取,检查已经拥有的锁每个线程缓存是否匹配对象,匹配的话进行lockCount++

3. 总结

@synchorized 2种存储方式tlscache,通过threadCountlockCount支持互斥递归多线程。日常使用频率比较高,不需要我们手动解锁,并且真机上效率也进行了优化。 不能使用非OC对象作为加锁对象,因为其object的参数为id,加锁的对象注意作用域,防止多线程下导致野指针出现。

  • 未完待续。。。。。