iOS 底层探索篇 —— 锁

1. 锁的简介

我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题。而合理的运用锁可以保证每次只有一个线程访问这一块资源,这样就可以解决多线程引发的数据问题。

2. ios 8大锁

将各种锁循环加锁开锁100000次,计算出耗费的时间来看一下各种锁的性能。从输出可以看到各种锁的性能差别。可以看出,锁的性能从高到底依次是:OSSpinLock(自旋锁) -> dispatch_semaphone(信号量) -> pthread_mutex(互斥锁) -> NSLock(互斥锁) -> NSCondition(条件锁) -> pthread_mutex(recursive 互斥递归锁) -> NSRecursiveLock(递归锁) -> synchronized(互斥锁) -> NSConditionLock(条件锁)

在这里插入图片描述

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

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

  • NSLock
  • pthread_mutex
  • @synchronized

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

  • NSCondition
  • NSConditionLock

递归锁:就是同一个线程可以加锁N次而不会引发死锁

  • NSRecursiveLock
  • pthread_mutex(recursive)

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

  • dispatch_semaphore

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

3. @synchronized

3.1 总体流程

在实际开发中,@synchronized用的最经常,范围也最广,所以先从@synchronized开始探索。

当开始多线程售票的时候,那么余票的数量就会因为多线程而发生数据错乱的问题。这个时候,就需要为其加上一把锁,这样就能保证数据的安全。
在这里插入图片描述
在这里插入图片描述
这时候就有了一些疑问。

  • @synchronized里面的参数应该传什么呢?这里为什么传self呢?是否可以传nil呢?
  • @synchronized代码块到底是什么呢?
  • @synchronized如何做到加锁以及递归可重入的呢?
  • @synchronized是什么结构呢?

先探索这些问题,那么就在main.m中添加@synchronized,然后用xcrun生成main.cpp文件。
在这里插入图片描述

在这里插入图片描述

生成main.cpp文件后打开,看到@synchronized在c中的结构。
在这里插入图片描述

这里如果加锁成功的话,就不需要看catch里面的代码。结构体_SYNC_EXIT的代码可以放在外面。id _sync_obj = (id)appDelegateClassName; 就是传的参数。那么需要看的就只剩下objc_sync_enter_sync_exit。_sync_exit是_SYNC_EXIT的析构函数,所以_sync_exit相当于objc_sync_exit。那么最终
@synchronized 相当于 objc_sync_enter 和 objc_sync_exit

在这里插入图片描述
在工程中下 objc_sync_enter 和 objc_sync_exit 断点,发现都在libobjc.A.dylib里面。
在这里插入图片描述
在这里插入图片描述
接下来就去libobjc.A.dylib里面探索。

这里看到objc_sync_enter,如果objc不存在会报出异常,也就是当如果传nil的话,就会报出异常。这里调用的objc_sync_nil也是没有做任何事情。BREAKPOINT_FUNCTION是一个封装,BREAKPOINT_FUNCTION( void objc_sync_nil(void))相当于 void objc_sync_nil(void){};也就是什么都没有实现。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
看到objc_sync_exit,这里如果obj不存在的话,那么也是不做处理。这里id2data传的第二个参数和objc_sync_enter不一样为RELEASE,objc_sync_enter为ACQUIRE。并且这里调用data->mutex.tryUnlock(),而objc_sync_enter则是data->mutex.lock()
在这里插入图片描述
接下来看SyncData的结构。看到里面有nextData,大致可以猜出这里是单向链表结构。这里的DisguisedPtr在关联对象有看到过,是把对象封装成统一的数据结构。threadCount则是代表着线程数量,说明可以多线程访问。recursive_mutex_t则是递归锁,但是recursive_mutex_t不能多线程递归,否则就会有bug。
在这里插入图片描述
然后看到id2data。这里做了一些变量的准备,然后判断data是否存在。这里的SUPPORT_DIRECT_THREAD_KEYS是是否支持tls(线程局部存储(Thread Local Storage,TLS) pthread_key_create(),是操作系统为线程单独提供的私有空间,通常只有有限的容量)。然后再判断cache是否存在。这里说明了有两个地方存储这个data。
在这里插入图片描述
往下这里则是开辟了一个新的SyncData,进行了内存对齐的操作,并对SyncData的值进行了初始化赋值
在这里插入图片描述
往下到了done,就进行了解锁,这里的锁是id2data这个方法的锁,然后返回result
在这里插入图片描述
看完总体流程,然后在从头看。开头变量的赋值是从sDataLists里面获取的。sDataLists是全局静态变量,具有全局性,是一个全局哈希表。SyncList里面则有data和lock
在这里插入图片描述
在这里插入图片描述 可以在代码里面打下断点后输出来查看数据结构。
在这里插入图片描述
这里最外面是array包了一层,因为是哈希结构所以第一次插入需要计算下标,这里是在[43]。
在这里插入图片描述

在这里插入图片描述
所以sDataLists的数据结构为下图所示。外面是全局的表,里面则有着SyncList,SyncList里面有着data 指向SyncData。这里syncList的下标是通过id2data方法的参数object得到的,如果哈希冲突的话,那么又会创建一个SyncData,通过链表的形式解决哈希冲突的问题。
请添加图片描述

往下看发现data和cache的逻辑是相似的,只是存储的数据结构不一样。如果支持tls,则走data,否则就走cache。所以这里只需要看一个地方就够了。
在这里插入图片描述

在这里插入图片描述

第一次进入这个方法时,data是为空的,那么就会走到下面创建SyncData的地方,这里会创建SyncData并且将其加入到list里面。 这里的两步操作 result->nextData = *listp; 和 *listp = result; 说明用的是头插法。
在这里插入图片描述
接下来到done里面。这里只有新的ACQUIRE才可以进来,RELEASErecursive ACQUIRE 应该在前面就被处理了。
在这里插入图片描述
当第二次进来这个方法时,SyncData就不为空了,就会走到if (data)里面。 这里的data时通过tls_get_direct获取的,而SYNC_DATA_DIRECT_KEY是一个宏定义是个固定的值,那么说明每次进来取得SyncData都是上一个存储的SyncData的data。这里如果object是相同的的,则会拿到这个对象已经锁了多少次也就是 lockCount,而lockCount说明了可递归。然后判断why,如果是ACQUIRE,则进行lockCount++操作。如果是RELEASE 则进行lockCount--操作,并且如果lockCount等于0也就是解锁完成了,那么就会进行线程数的--操作,这里也说明了可以在多线程使用。如果是CHECK就什么都不做。

3.2 同一个对象不同线程

接下来实际操作一下同一个对象不同线程里面加锁的情况。
在这里插入图片描述
这里第一次进来创建syncData,并且进行初始化赋值,然后将result也就是刚创建的syncData存到tls,并将lockCount设为1存入到tls
在这里插入图片描述
在这里插入图片描述
输出result看到threadCount是为1的。
在这里插入图片描述

第二次进来则进入到这里,进行threadCount的++操作.然后将result也就是处理过的syncData存到tls,并将lockCount设为1存入到tls
在这里插入图片描述
在这里插入图片描述

输出result看到这里threadCount变成了2
在这里插入图片描述

3.3 同一个对象同一线程

接下来实际操作一下同一个对象同一线程里面加锁的情况。
在这里插入图片描述
当第一次进来的时候,和之前的情况一下。需要看的是第二次进来的时候的情况。
这里就会进来判断data是否存在,然后根据传进来的why进行相对应的操作,这里是进行++。
在这里插入图片描述

3.4 不同对象同一线程

接下来实际操作一下不同对象同一线程里面加锁的情况。

在这里插入图片描述

当第一次进来的时候,和之前的情况一下。需要看的是第二次进来的时候的情况。
发现第二次进来,会重新创建syncData并进行初始化赋值。
在这里插入图片描述
但是在done里面,会把创建的syncData存入到cache里面而不是tls。
在这里插入图片描述

3.5 不同对象不同线程

接下来实际操作一下不同对象不同线程里面加锁的情况。
在这里插入图片描述

当第一次进来的时候,和之前的情况一下。需要看的是第二次进来的时候的情况。
发现第二次进来,会重新创建syncData并进行初始化赋值。
在这里插入图片描述
输出sDataLists发现两个存在了不同的地方,说明没有哈希冲突。
在这里插入图片描述
这里修改一下StripedMap的值来模拟一下哈希冲突的情况。
在这里插入图片描述
这里输出后,发现第二次创建的syncData的nextData有值了,并且是第一个syncData,说明了这里使用的的拉链法解决的哈希冲突。
在这里插入图片描述

流程总结

  • 首先在tls即线程缓存中查找。通过tls_get_direct去获取SyncData。
  • 判断获取的data是否存在,并且判断data里面的object是否等于传进来的object
  • 如果相等则获取lockCount,就是对象被锁的次数(即锁的嵌套次数)
  • 如果data中的threadCount 小于等于0,或者 lockCount 小于等于0时,则直接崩溃
  • 通过传入的why,判断是什么操作类型
  1. 如果是ACQUIRE,表示加锁,则进行lockCount++,并保存到tls缓存
  2. 如果是RELEASE,表示释放,则进行lockCount–,并保存到tls缓存。如果lockCount 等于 0,从tls中移除线程data,并且对threadCount进行–操作
  3. 如果是CHECK,则什么也不做
  • 如果tls中没有,则在cache缓存中查找
  • 通过fetch_cache方法查找cache缓存中是否有线程
  • 如果有,则遍历cache总表,读取出线程对应的SyncCacheItem
  • 从SyncCacheItem中取出data,然后后续步骤与tls里面的操作是一致的
  • 如果listp存在但是tls中以及cache中没有,说明是在另外一个线程,就会判断listp里面的object和传进来的object是否一样,一样的话就进行threadCount ++ 的操作然后去done里面把syncData存在tls,lockCount设为1存在tls。
  • 如果以上情况都不是,也就是第一次进来。则创建SyncData,然后进行相对应的初始化赋值,然后到done里面里面把syncData存在tls,lockCount设为1存在tls。

所以在id2data方法中,主要分为5种情况

  1. 第一次进来,没有锁
    创建syncData,进行初始化赋值,然后将syncData存储到tls, lockCount 设为1 存储到tls。

  2. 不是第一次进来,且是同一对象,同一个线程
    tls中有数据,则判断是否为同一对象,同一对象则根据传进来的why进行相对应的操作后存储到tls。

  3. 不是第一次进来,且是同一对象,不同线程
    是同一对象,进行threadCount++操作,然后将syncData存入到tls, lockCount 设为1 存储到tls。

  4. 不是第一次进来,且是不同对象,不同线程
    创建syncData,进行初始化赋值,然后将syncData存储到tls, lockCount 设为1 存储到tls,如果哈希冲突就用拉链法,否则就是根据哈希算出来的下标存入到sDataLists。

  5. 不是第一次进来,且是不同对象,同一线程
    创建syncData,进行初始化赋值,然后将syncData存储到cache, lockCount 设为1 存储到cache。

4. @synchronized总结

  • sync是全局的哈希表,采用的是拉链法
  • sDataList array 存的是SyncList,SyncList版订的是object,也就是要加锁的对象。
  • 底层是 objc_sync_enter / exit 对称 递归互斥锁
  • 两种存储,tls以及cache存储
  • 第一次来的时候,就会创建一个syncData,用头插法创建一个链表结构,标记threadCount为1
  • 第二次来的时候判断是否为同一个对象,如果是的话就会在TLS里面对lockCount进行++操作。
  • TLS 找不到,重新创建syncData,然后对 threadCount进行 ++操作
    -如果exit的话,就会对lockCount进行-- ,当lockCount为0时,则会对threadCount进行–操作
  • Synchronized : 可重入 递归 多线程是因为有threadCount记录多少条线程对这个锁对象加锁,lockCount 记录在这个线程里面锁了多少次
  • 注意事项,@synchronized传的对象不能为nil,否则加锁失败。@synchronized传的对象也不要为临时变量,因为临时变量生命周期只会在当前作用域。
  • 传self 的原因:
  1. 生命周期
  2. 只会形成一条拉链,方便存储和释放。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值