美文网首页iOS开发
iOS @synchronized的底层原理

iOS @synchronized的底层原理

作者: 远方竹叶 | 来源:发表于2020-11-23 10:31 被阅读0次

    iOS 开发常见的几种锁 介绍了常见的几种锁的使用场景以及使用方法,它的底层是如何实现的呢?下面我们带着疑问一起去探索下 @synchronized 的底层原理吧

    @synchronized

    开发中,在多个线程访问同一块资源的时候,我们会添加以下代码来避免引发数据错乱和数据安全的问题

    @synchronized (self) {
        //添加执行的代码
    }
    

    那我们如何去探索它的底层实现呢?首先我们需要它在底层会调用什么方法,其次我们要知道方法所在的源码库,这样我们才能清晰的知道它的底层是如何实现的。

    底层方法调用探究

    通过开启汇编调试,我们看到如下 @synchronized 在执行过程中,会走底层的objc_sync_enterobjc_sync_exit 方法

    此时我们对 objc_sync_enter 方法下一个符号断点,发现底层实现所在的源码库是 libobjc.A.dylib

    objc_sync_enter & objc_sync_exit

    打开 objc-781 源码工程,查看 objc_sync_enter 的源码实现如下:

    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        if (obj) {
            // 执行 ACQUIRE 操作,返回 data 数据
            SyncData* data = id2data(obj, ACQUIRE);
            ASSERT(data);
            // 加锁
            data->mutex.lock();
        } else {
            // @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_exit 的源码,实现如下:

    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        
        if (obj) {
            // 执行 RELEASE 操作,返回 data 数据
            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 {
            // @synchronized(nil) does nothing
        }
        
    
        return result;
    }
    

    从源码中我们可以看到,如果传入的 objnil,则什么都不做;如果传入的 obj 不为 nil,则会获取相应的 SyncData 对它进行一系列的操作。那么这个 SyncData 是什么?它的结构是什么样的?SyncData 的定义如下:

    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;
    

    SyncData 的定义可以看到,它是一个结构体。第一个成员变量指向下一个 SyncData,是一个链表结构;第四个成员属性代表递归属性。从 SyncData 结构就可以看出 @synchronized 是一个递归互斥锁。

    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;
    

    这里顺便查看 SyncCache 的结构,后续会调用到。SyncCache 是一个结构体对象,用于存储线程。其中 list[0] 表示当前线程的链表 data,主要用于存储 SyncDatalockCount

    id2data 源码分析

    这里源码很长,先总体看下流程,后面详细分析

    整体分为四大步

      1. 快速缓存

    如果支持快速缓存,就从快速缓存中读取线程和任务,再进行相应操作

      1. 线程缓存

    快速缓存没找到,就从线程缓存中读取线程和任务,再进行相应操作

    上述代码中 fetch_cache 函数进行缓存查询和开辟

      1. 循环遍历

    所有的缓存都找不到,循环遍历每个线程和任务,再进行相应操作

      1. Done

    如果有错误,则抛出异常;如果正常,则存入快速缓存(前提是支持快速缓存)和线程缓存中,便于下次快速查找

    每个被锁的 object 对象可拥有一个或多个线程

    拓展

    以下代码,运行后会发生什么?

    _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];
            }
        });
    }
    

    我们运行项目,看看会发生什么

    项目运行就崩溃了,原因在与 self.testArray 会触发 set 方法,而 set 方法本质是新值 retain,旧值 release,而在异步调用时,可能会造成多次调用 release(上一次的 release 还没结束,下一次的 release 已经来了),导致野指针,从而 crash

    • 验证

    我们打开Xcode 工程 -> Edit Scheme... ->Run -> diagnostics -> 勾选 Zombie Objects,再次运行项目

    调用 [__NSArrayM release] 时,是发送给了 deallocated (已析构释放)的对象。

    僵尸对象是一种用来检测内存错误(EXC_BAD_ACCESS)的,给僵尸对象发送消息时,那么将在运行期间崩溃和输出错误日志。通过日志可以定位到野指针对象调用的方法和类名。

    • 添加 @synchronized

    既然我们知道了原因,那我们使用 @synchronized 加锁试一下吧(这是 @synchronized 错误示范😄)

    NSLog(@"123");
    _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];
           }
       });
    }
    

    再次运行项目,还是 crash 了,报错信息和上面的一致,这是为什么呢?在上面的源码分析中可以看到,因为锁的对象是 self.testArray,它会 release,它释放了,等于锁也就没用了。

    正确的使用方法

    NSLog(@"123");
    _testArray = [NSMutableArray array];
    for (int i = 0; i < 200000; i++) {
       dispatch_async(dispatch_get_global_queue(0, 0), ^{
           @synchronized (self) {
               self.testArray = [NSMutableArray array];
           }
       });
    }
    

    @synchronized 锁的对象,需要确保锁内代码的生命周期。所以将锁对象改为self。就解决问题了。当然也可以用其他锁来解决问题

    相关文章

      网友评论

        本文标题:iOS @synchronized的底层原理

        本文链接:https://www.haomeiwen.com/subject/plrbbktx.html