阅读 227
iOS底层  - @synchronized 流程分析

iOS底层 - @synchronized 流程分析

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

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用

以上内容的总结专栏


细枝末节整理


前言

上一篇,我们分析了锁的基本使用,接下来我们逐一将iOS开发中常用的锁做一个探索,今天我们就先来看看 @synchronized 这把互斥锁的流程吧。

@synchronized 流程分析

准备

线程局部存储 (Thread Loacl Storage, TLS)

是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下 通常通过 pthread 库中的

  • pthread_key_create()
  • pthread_getspecific()
  • pthread_setspecific()
  • pthread_key_delete()

posix_memalign 支持内存对齐


编译定位源码位置

想要知道其底层结构,那么,常规操作 xcrun 一下, 在.cpp文件中得到以下内容:

  • main函数中的定义
@synchronized (appDelegateClassName) { }
复制代码
  • 编译后的代码:
    {
        id _rethrow = 0;
        id _sync_obj = (id)appDelegateClassName; 
        
        objc_sync_enter(_sync_obj);
            
        try {
        
            // 结构体
            struct _SYNC_EXIT {
            // 构造函数
            _SYNC_EXIT(id arg) : sync_exit(arg) {}
            // 析构函数
            ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
             id sync_exit;
                    
           } 
           
           _sync_exit(_sync_obj);
                
        } catch (id e) {_rethrow = e;}
            
        { struct _FIN { _FIN(id reth) : rethrow(reth) {}
                ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                id rethrow;
        } _fin_force_rethow(_rethrow);}
    }
复制代码

简单分析一下,我们发现重点内容如下:

  • try{} 代码块中的内容。
  • objc_sync_enter(_sync_obj);
  • objc_sync_exit(sync_exit);

全局搜索没有 没有找到以上内容的具体实现内容,接下来,我们通过符号断点来看下 @synchronized 是在哪一个库中实现:

image.png

可以看到其具体实现是放在 libobjc.A.dylib 中。

下面我们通过 libobjc 源码 来分析下其内部实现的流程。

流程探索

objc_sync_enter

// 在'obj'上开始同步
// 如果需要,分配与'obj'关联的递归互斥锁
// 一旦获得锁,返回OBJC_SYNC_SUCCESS  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        
        // obj 存在
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
        
    } else {
        // obj 不存在 什么事情也不做
        // @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;
}

...

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

...
// 什么也没有实现
/* 对于打算作为断点钩子的函数使用此方法
   如果不这样做,编译器可能会对它们进行优化
   BREAKPOINT_FUNCTION( void stop_on_error(void) ); */
#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }
    
...
复制代码

objc_sync_exit

// 在'obj'上结束同步 
// 返回 OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        
        //obj 存在
        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 不存在 什么事情也不做
        // @synchronized(nil) does nothing
    }
	

    return result;
}
复制代码

可以看到 在 objc_sync_enterobjc_sync_exit 中 对于 被锁对象obj 为空的时候都是什么事情也不会做。

在obj存在的情况下, 两者也分别调用了 id2data 这个函数, 不过 前者传参数 ACQUIRE, 后者则是 RELEASE, 接下来的操作不同的是,前者是 进行了 data->mutex.lock(); 加锁操作, 后者 则是 data 存在的时候 data->mutex.tryUnlock(); 解锁的操作。 一个加锁,一个解锁 这也正是锁的常规操作。

这里我们那还应该关注下 data 的结构, 也就是 SyncData,因为 是这个数据中封装的锁做的加锁解锁的操作。

下面,我们先解读下 SyncData 的数据结构,然后再重点看下 id2data 这个函数的内部实现来 继续 探索 @synchronized 这把互斥锁的底层实现流程。

SyncData 数据结构

SyncData 是一个结构体:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;     // 使用此块的线程数
    recursive_mutex_t mutex; //递归锁
} SyncData;
复制代码
  • 当看到第一个 nextData 指针的时候,我猜想 这个 SyncData 是一个 单向链表结构(因为 SyncData 的结构会通过 nextData 指向下一个SyncData数据 )。

  • threadCount 记录使用线程的数,也就是说支持多线程。 那么,具体是如何实现的多线程也可以使用 @synchronized 呢? 我们接下来通过流程分析来看一下。

id2data 流程分析


struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

// 全局静态变量   哈希表
static StripedMap<SyncList> sDataLists;

// 哈希函数 确定一个下标
// 可能会发生冲突 (之前通过在哈希来解决)
// 这里 使用 拉链法

...

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
    //支持线程局部存储
    
    // 检查每个线程的单条目快速缓存是否匹配对象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    
    // 第一部分
    if (data) { ... }
    
#endif
    
    // 检查已经拥有的锁的每个线程缓存是否匹配对象
    SyncCache *cache = fetch_cache(NO);
    
    // 第二部分
    if (cache) { ... }
    
    //线程缓存没有找到任何东西
    //遍历正在使用的列表以查找匹配的对象
    //自旋锁防止多个线程创建多个
    //锁定相同的新对象
    //我们可以把节点保存在某个哈希表中,如果有的话 
    //有20多个不同的锁在工作中,但我们现在不这么做。
    
    
    //这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦, 此处是一个 spinlock_t 在上面有定义
    lockp->lock(); 
    
    // 第三部分
    // 代码块
    { ... }
    
    // 分配一个新的SyncData并添加到列表中.
    // XXX在持有全局锁的情况下分配内存是不好的做法,
    // 可能值得释放锁,重新分配,再搜索一次.
    // 但由于我们从不释放这些人我们不会经常陷入分配中.
    
    // 第四部分
    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(); //这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦,此处是一个 spinlock_t 在上面有定义
    
    // 第五部分
    if (result) { ... }
    
    return result;
}
复制代码

第一部分 和 第二部分

同一个线程会来到这里处理。

如果支持线程局部存储 则 通过线程局部存储来返回 SyncData, 如果不支持, 则 通过线程缓存 来存取 SyncData;

从 TLS 中 获取到 data(SyncData), 然后 判断下 why 处理下ACQUIRE和RELEASE 然后将data return 出去。

第一部分

if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            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: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case 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;
        }
    }
复制代码

和第一部分流程相似,不过是从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->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
复制代码

第三部分

不同的线程会在这里处理。

当我们 锁的对象 第一次进来 id2data 流程的时候, 第三部分并不会做什么事情。

{
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // 原子的,因为可能会与并发的RELEASE冲突
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // 没有当前与对象关联的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // 找到一个没用过的,就用它
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
复制代码

第四部分

result 是当前 从 TLS 或者 缓存中 通过 被锁对象object 拿到的 SyncData。 将其 nextData 指向 了自己;这里涉及到一个 头插法。 也就是每次新来的数据会插在之前数据的前边。

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;
复制代码

第五部分

对之前查询到的数据 进行 TLS 或者 线程缓存的存储

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) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)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++;
        }
    }
复制代码

LLDB 调试分析

我们自定义一个使用 @synchronized 的例子来调试一下:

    SMPerson *p1 = [SMPerson alloc];
    SMPerson *p2 = [SMPerson alloc];
    SMPerson *p3 = [SMPerson alloc];
    SMPerson *p4 = [SMPerson alloc];
    SMPerson *p5 = [SMPerson alloc];
    SMPerson *p6 = [SMPerson alloc];
        
    dispatch_async(dispatch_queue_create("superman", DISPATCH_QUEUE_CONCURRENT), ^{
            NSLog(@"0");
        @synchronized (p1) {
                NSLog(@"1");
            @synchronized (p2) {
                    NSLog(@"2");
                 @synchronized (p1) {
                     NSLog(@"1-1");
                     @synchronized (p1) {
                         NSLog(@"1-1-1");
                         @synchronized (p3) {
                             NSLog(@"3");
                             @synchronized (p4) {
                                 NSLog(@"4");
                                 @synchronized (p5) {
                                        NSLog(@"5");
                                        @synchronized (p6) {
                                            NSLog(@"6");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }        
    });
复制代码

第一次进来时 obj = p1; TLS 和 缓存 中 均为空。

会来到 第四部分(初始化一个新的SyncData并添加到列表中) 和 第五部分(对 tls 和 缓存 做存储) 内部实际做了 tls 的存储 处理。

第二次进来时 obj = p2;

在 TLS 或 缓存中 拿到了上一次加锁的 SyncData,

流程和上面的一样,不过在第五部分是走的 线程缓存,没走 tls 的存储

第三次进来 obj = p1;

会来到 第一部分 执行,判断时上一次的 对象一样的话向下执行, 进入 ACQUIRE 分支, 对 lockCount++; 对 将锁的次数通过 SYNC_COUNT_DIRECT_KEY 在 tls 存储, 并返回 tls 存储中取出来的 SyncData。 下次进来判断时同一个对象的话,也会从 SYNC_COUNT_DIRECT_KEY 取出来 锁的次数。

第四次进来 obj = p1;

执行顺序和上一次的一样。

总结

  • @synchronized 这把锁在 使用中, 会在维护一张全局的哈希表(static StripedMap sDataLists;), 使用拉链法来存储 SyncData;
  • sDataLists, 中array 存储的是 SyncList (绑定的是objc,我们加锁的对象);
  • objc_sync_enter 和 objc_sync_exit 调用对称,封装的是递归锁;
  • 两种存储方式 TLS 和 线程缓存;
  • 第一次 SyncData采用头插法 在链表结构中, 标记 threadCount = 1;
  • 判断是否同一个对象:如果是,lockCount++; 否: 重复上一步。
  • lockCount--,lockCount == 0 : threadCount--;

用一张图来总结以上流程:

@synchronized 流程分析.001.jpeg

@synchronized 为什么 具备可重入、 可递归、 可多线程?

因为: TLS 存储了 threadCount 标记 线程数; lockCount 标记了加锁次数; 对锁对象的加锁。

关于 SyncList 结构

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

// 全局静态变量   哈希表
static StripedMap<SyncList> sDataLists;

// 哈希函数 确定一个下标
// 可能会发生冲突 (之前通过在哈希来解决)
// 这里 使用 拉链法

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;     // 使用此块的线程数
    recursive_mutex_t mutex; //递归锁
} SyncData;

复制代码

id2data 流程分析 中,有关于 sDataLists 的底层结构,在不同的架构环境中容量稍有不同, SyncList 存储在其中, SyncList中存储 了 SyncData,而SyncData是一个链表的结构。由此,形成了一下的拉链结构:

SyncList 分析.001.jpeg

关于 TLS 和 cache

在第五部分的 代码中 :

{    
    // Save in thread cache 
    if (!cache)
    cache = fetch_cache(YES); 
    cache->list[cache->used].data = result;
    cache->list[cache->used].lockCount = 1;
    cache->used++;
}
复制代码

这一部分的细节: 如果是同一个线程在操作 @synchronized 锁的话,不能持续的对 TLS 进行 无限制串处理(会对TLS造成很大的压力),所以,只有在 切换线程的时候才会去 TLS, 其他情况 都会来到 thread cache (内存缓存)来存储。

文章分类
iOS
文章标签