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之深入解析“锁”的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析YYModel的底层原理

iOS之深入解析渲染的底层原理

iOS之深入解析通知NSNotification的底层原理

iOS之深入解析malloc的底层原理

iOS之深入解析KVO的底层原理

iOS之深入解析分类Category的底层原理