iOS之深入解析“锁”的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析“锁”的底层原理相关的知识,希望对你有一定的参考价值。
一、OSSpinLock(自旋锁)
- 自从 OSSpinLock 出现安全问题,在 ios10 之后就被 Apple 废弃。自旋锁之所以不安全,是因为获取锁后,线程会一直处于忙等待,造成了任务的优先级反转;
- 其中的忙等待机制可能会造成高优先级任务一直 running 等待,占用时间片,而低优先级的任务无法抢占时间片,会造成一直不能完成,锁未释放的情况;
- 在 OSSpinLock 被弃用后,其替代方案是内部封装 os_unfair_lock,而 os_unfair_lock 在加锁时会处于休眠状态,而不是自旋锁的忙等待状态。
二、atomic(原子锁)
- atomic 适用于 OC 中属性的修饰符,其自带一把自旋锁,但是一般基本不使用,都是使用的 nonatomic;
- setter 方法会根据修饰符调用不同方法,其中最后会统一调用 reallySetProperty 方法,其中就有 atomic 和非 atomic 的操作;
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
...
id *slot = (id*) ((char*)self + offset);
...
if (!atomic) { // 未加锁
oldValue = *slot;
*slot = newValue;
} else { // 加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
...
}
- 从源码中可以看出,对于 atomic 修饰的属性,进行了 spinlock_t 加锁处理,但 OSSpinLock 已经被废弃,这里的 spinlock_t 在底层是通过 os_unfair_lock 替代了 OSSpinLock 实现的加锁,同时为了防止哈希冲突,还是使用了“加盐”操作。
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
...
}
- getter 方法中对 atomic 的处理,同 setter 是大致相同的,如下:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock(); // 加锁
id value = objc_retain(*slot);
slotlock.unlock(); // 解锁
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
三、synchronized(互斥递归锁)
- 开启汇编调试,发现 @synchronized 在执行过程中,会执行底层的 objc_sync_enter 和 objc_sync_exit 方法:
- 通过clang,查看底层编译代码,如下:
- 通过对 objc_sync_enter 方法符号断点,查看底层所在的源码库,断点调试可以看到它在 objc 源码中,即 libobjc.A.dylib:
① objc_sync_enter 分析
- 进入 objc_sync_enter 源码实现,如果 obj 存在,则通过 id2data 方法获取相应的 SyncData,对 threadCount、lockCount 进行递增操作;如果 obj 不存在,则调用 objc_sync_nil:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) { // 传入不为nil
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
data->mutex.lock(); // 加锁
} else { // 传入nil
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
- 通过符号断点得知,objc_sync_nil 方法里面什么都没做,直接被 return;
② objc_sync_exit 分析
- 在 objc_sync_exit 源码中,可以看到,如果 obj 存在,则调用 id2data 方法获取对应的 SyncData,对 threadCount、lockCount 进行递减操作;
如果 obj 为 nil,底层并没有执行任何操作,如下:
// End synchronizing on 'obj'. 结束对“ obj”的同步
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) { // obj不为nil
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock(); // 解锁
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else { // obj为nil时,无任何操作
// @synchronized(nil) does nothing
}
return result;
}
- 通过上面两个底层实现的对比,可以看到它们有一个共同点,在 obj 存在时,都会通过 id2data 方法,获取 SyncData。
- 继续进入 SyncData 的定义,是一个结构体,主要用来表示一个线程 data,类似于链表结构,有 next 指向,且封装 recursive_mutex_t 属性,可以确认 @synchronized 的的确确是一个递归互斥锁,如下:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // 类似链表结构
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex; // 递归锁
} SyncData;
- 进入 SyncCache 的定义,也是一个结构体,用于存储线程,其中 list[0] 表示当前线程的链表 data,主要用于存储 SyncData 和 lockCount,如下:
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
③ id2data 分析
- 进入 id2data 源码,可以看出,这个方法是加锁和解锁都复用的方法:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的线程缓存)
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
// 通过KVC方式对线程进行获取 线程绑定的data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
// 如果线程缓存中有data,执行if流程
if (data) {
fastCacheOccupied = YES;
// 如果在线程空间找到了data
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
// 通过KVC获取lockCount,lockCount用来记录 被锁了几次,即 该锁可嵌套
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: {
// objc_sync_enter走这里,传入的是ACQUIRE -- 获取
lockCount++; // 通过lockCount判断被锁了几次,即表示 可重入(递归锁如果可重入,会死锁)
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);// 设置
break;
}
case RELEASE:
// objc_sync_exit走这里,传入的why是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;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO); // 判断缓存中是否有该线程
// 如果cache中有,方式与线程缓存一致
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) { // 遍历总表
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE: // 加锁
item->lockCount++;
break;
case RELEASE: // 解锁
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache 从cache中清除使用标记
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
// 第一次进入,无缓存
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {//cache中已经找到
if ( p->object == object ) { // 如果不等于空,且与object相似
result = p; // 赋值
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount); // 对threadCount进行++
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object 没有与当前对象关联的SyncData
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it 第一次进来,没有找到
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); // 创建赋值
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) { //判断是否支持栈存缓存,支持则通过KVC形式赋值 存入tls
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
} else
#endif
{
// Save in thread cache 缓存中存一份
if (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
- tls 在线程缓存中查找:
- 在 tls_get_direct 方法中以线程为 key,通过 KVC 的方式获取与之绑定的 SyncData,即线程 data(其中的 tls(),表示本地局部的线程缓存);
- 判断获取的 data 是否存在,以及判断 data 中是否能找到对应的 object;
- 如果存在,在 tls_get_direct 方法中以 KVC 的方式获取 lockCount,用来记录对象被锁了几次(即锁的嵌套次数);
- 如果 data 中的 threadCount 小于等于0,或者 lockCount 小于等于0时,则直接崩溃;
- 通过传入的 why,判断是操作类型:
- 如果是 ACQUIRE,表示加锁,则进行 lockCount++,并保存到 tls 缓存;
- 如果是 RELEASE,表示释放,则进行 lockCount–,并保存到 tls 缓存,如果 lockCount 等于 0,从 tls 中移除线程 data;
- 如果是CHECK,则无任何操作。
- 如果 tls 中不存在,则在 cache 缓存中继续查找:
- 通过 fetch_cache 方法查找 cache 缓存中是否有线程;
- 如果有,则遍历 cache 总表,读取出线程对应的 SyncCacheItem;
- 从 SyncCacheItem 中取出 data,然后后续步骤与 tls 的匹配是一致的;
- 如果 cache 中也没有,即第一次进来,则创建 SyncData,并存储到相应缓存中:
- 如果在 cache 中找到线程,且与 object 相等,则进行赋值、以及threadCount++;
- 如果在 cache 中没有找到,则 threadCount 等于1;
- 在 id2data 方法中,主要存在三种情况:
- 第一次执行 id2data 方法,没有锁,则 threadCount = 1,lockCount = 1,并存储到 tls;
- 不是第一次执行 id2data 方法,但是同一个线程,则 tls 中有数据,则lockCount++,存储到 tls;
- 不是第一次执行 id2data 方法,并且是不同的线程,全局线程空间进行查找线程,threadCount++,lockCount++,存储到 cache。
④ tls 和 cache 表结构
- tls 哈希表的底层的表结构如下所示:
- cache 缓存,底层的表结构如下所示:
- 哈希表结构中通过 SyncList 结构来组装多线程的情况;
- SyncData 通过链表的形式组装当前可重入的情况;
- 底层通过 tls 线程缓存、cache 缓存来进行处理;
- 底层主要是 lockCount 和 threadCount 解决递归互斥锁和嵌套可重入的情况。
⑤ @synchronized 易错点
- 现有如下代码,会出现什么问题?
- (void)ydw_testSync {
self.testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
}
- 执行程序,可以看到出现了崩溃:
- 崩溃的原因是在于 testArray 在某一瞬间变成了nil,从 @synchronized底层流程分析可以知道,如果加锁的对象变成 nil,无法保证线程被锁(即线程安全),相当于下面的这种情况,block 内部不停的 retain、release,会在某一瞬间上一个还未被 release,下一个已经准备被 release,这样会导致野指针的产生:
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
- 可以根据上面的代码,打开 Products -> Scheme -> edit scheme -> run -> Diagnostics 中勾选 Zombie Objects ,来查看是否是僵尸对象,结果如下所示:
*** -[__NSArrayM release]: message sent to deallocated instance 0x600000d904e0
- 一般在使用 @synchronized (self),_testArray 的持有者是 self;
- 注意:野指针是指由于过渡释放产生的指针还在进行操作;过渡释放是指每次都会 retain 和 release;
⑥ 总结
- @synchronized 在底层是一把递归锁,因此 @synchronized 是递归互斥锁;
- @synchronized 的可重入,即可嵌套,主要是由于 lockCount 和 threadCount 的搭配;
- @synchronized 使用链表主要由于链表方便下一个 data 的插入;
- 由于底层中链表查询、缓存的查找以及递归,是非常耗内存以及性能的,会导致性能低;
- 目前该锁的使用频率仍然很高,主要是因为方便简单,且不用解锁;
- 不能使用非 OC 对象作为加锁对象,因为其 object 的参数为 id;
- @synchronized (self) 适用于嵌套次数较少的场景,锁住的对象也并不永远是 self;
- 如果锁嵌套次数较多,即锁 self 过多,会导致底层的查找非常麻烦,因为其底层是链表进行查找,所以会相对比较麻烦,所以此时可以使用NSLock、信号量等。
四、NSLock
① NSLock 的底层原理
- 通过符号断点 lock 分析,发现其源码在 Foundation 框架中:
- 由于 OC 的 Foundation 框架不开源,所以这里借助 Swift 的开源框架 Foundation 来分析 NSLock 的底层实现,其原理与 OC 是大致相同的:
- 通过源码实现可以看出,底层是通过 pthread_mutex 互斥锁实现的。并且在 init 方法中,还做了一些其他操作,所以在使用 NSLock 时需要使用 init 初始化。
② NSLock 的弊端
- 请问下面 block 嵌套 block 的代码中,会有什么问题?
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
- 在未加锁之前,其中的 current = 9、10有很多条,导致数据混乱,主要原因是多线程导致的:
2021-05-11 19:34:22.323997+0800 @synchronized分析[30287:11176515] current value = 10
2021-05-11 19:34:22.324002+0800 @synchronized分析[30287:11176512] current value = 10
2021-05-11 19:34:22.324012+0800 @synchronized分析[30287:11176516] current value = 10
2021-05-11 19:34:22.324022以上是关于iOS之深入解析“锁”的底层原理的主要内容,如果未能解决你的问题,请参考以下文章