synchronized分析

225 阅读9分钟

以下内容仅是学习synchronized过程中的一些笔记, 没有进行思维逻辑的整理,所以读起来跳跃性可能较大,不过不妨碍理解 synchronized的一些原理性知识点,这里可以带着以下几个问题阅读源码:

  1. 锁是如何与传入 @synchronized 的对象关联上的
  2. 是否会对关联的对象有强引用
  3. 如果synchronize传入nil会有什么问题
  4. 假如传入 @synchronized 的对象在 @synchronized 的 block 里面被释放或者被赋值为 nil 将会怎么样
  5. synchronized是否是可重入的,即是否为递归锁 寻找底层实现入口一般有clang编译和汇编断点两种方式, 这里使用clang进行编译

通过clang 编译出的cpp显示,@synchronized(self){int a = 10;}最终转换成了以下代码:

{
     // 对象 
        id _rethrow = 0;
        id _sync_obj = (id)self;
    
    // 同步 
        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, 当调用析构函数时执行objc_sync_exit
                _sync_exit(_sync_obj);
            
                // 执行任务
                int a = 10;
            }
        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);
        }
    }

相比其他同步锁, 做了异常捕获相关的嵌入代码, 在没有成对处理锁时,不会直接死锁crash,而是抛出异常

注意到的两个核心方法就是 objc_sync_enter和objc_sync_exit

/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 * @param obj The object to begin synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS once lock is acquired.  
 */
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

/** 
 * End synchronizing on 'obj'. 
 * 
 * @param obj The object to end synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

enum {
    OBJC_SYNC_SUCCESS                 = 0,
    OBJC_SYNC_NOT_OWNING_THREAD_ERROR = -1
};

注释中需要注意的是objc_sync_enter是通过pthread_mutex 递归锁实现的

两个函数源码可以再objc/objc-sync中找到

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        // 使用关联对象创建节点
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        // 节点内上锁
        data->mutex.lock();
    }

    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        // ...
    } 
    return result;
}

无论是enter还是exit,其实核心代码都是id2data(),通过传入不同的枚举值表示不同的逻辑分支

id2data主要处理的内容是使用SyncData链表结构采用拉链法解决哈希冲突, 在缓存处理上支持了TLS/SyncCache 两种方式,

主要逻辑如下

  1. 查找当前线程的tls快速缓存, TLS由于是线程私有,所以直接使用对应的key存储lockCount和data, 这里需要注意fastCacheOccupied的判断条件, 只要存在一个SyncData就会为yes, 所以tls存储的一直都是链表的首节点,后续的SyncData都是存储到SyncCache中
  2. 查找syncCache缓存, fetch_cache的底层仍然是通过tls存储的, 所以该缓存同样也是线程隔离的, 同时又依赖于SyncCacheItem针对每一个线程存储对应的lockCount和data, 和第一步目的是一样的, 保证不同关联对象之间数据的隔离
  3. 查找全局字典syncDataLists, 该字典是一个全局字典, 通过将对象地址做偏移映射到对应index的syncData
  4. 如果仍未找到, 则创建新的syncData, 先判断是否存在未使用的syncData, 存在直接使用,不存在创建对应数据结构,采用头插法插入到syncDataLists 当前对象拉链中的头结点位置
  5. 将新的syncData缓存到tls/ syncCache中, 下次使用

具体逻辑并不是很复杂, 可以直接看代码中的注释

static SyncData* id2data(id object, enum usage why)
{
    // 通过object 获取dataList中的 data和 lock
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    // 通过tls存储对应的key, tls是线程私有的小容量存储空间, 是一个字典
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
  // =====================================存在快速缓存 Syncronized的数据结构, 在快速缓存中查找
        fastCacheOccupied = YES;

        // 递归的话, 则只处理计数即可, SyncData本身是递归锁
        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;
        }
    }
#endif
// ==================快速缓存没匹配, 在SyncCache缓存中查找
    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        // 遍历cache中所有在使用的对象. 匹配到本次加锁的对象
        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
                    // 和OSAtomicIncrement32Barrier同理, 防止共享资源冲突
                    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;
        // 遍历字典sDataList中 匹配本次加锁对象的数据结构, 每一个对象存储的是一个链表
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                // 这里使用原子操作, 防止多线程增加threadCount时和RELEASE操作冲突, 因为加锁只是当前逻辑部分加锁, 可能另一个线程正在进行上面的缓存中同一个关联对象的RELEASE操作
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            
            // 存在SyncData 节点的关联线程已经不存在, 在作为头节点使用, 性能优化
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        // 没有和当前对象关联的SyncData 对象, 如果是release 或者check则直接执行done, 其他例如ACQUIRE创建, 则新建result对象/或者使用旧的数据结构
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        // 使用旧的数据结构存储新的数据,不再需要创建result, 直接进行done操作
        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.
    // 创建新的result操作, 这里采用头插发
    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) {
            // 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++;
        }
    }

    return result;
}

几个数据结构成员变量的作用可以参考以下注释

typedef struct alignas(CacheLineSize) SyncData {
    // 下一个节点, SyncData可以理解成链表结构
    struct SyncData* nextData;
    
    // synchronize关联的对象
    DisguisedPtr<objc_object> object;
    
    // 使用这个block的线程数, 因为 SyncData 结构体会被缓存,threadCount==0 就暗示了这个 SyncData 实例可以被复用。
    int32_t threadCount;  // number of THREADS using this block
    
    // 和对象关联的递归锁, 执行代码块时真正生效的锁, 所以synchronize是一个可递归的锁
    recursive_mutex_t mutex;
} SyncData;

// 用于缓存相关
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block 这个锁锁定该block的次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

/*
  Fast cache: two fixed pthread keys store a single SyncCacheItem. 
  This avoids malloc of the SyncCache for threads that only synchronize 
  a single object at a time.
  SYNC_DATA_DIRECT_KEY  == SyncCacheItem.data
  SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
 */

// 正如上面提过,你可以把 SyncData 当做是链表中的节点。每个 SyncList 结构体都有个指向 SyncData 节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。
struct SyncList {
    // 节点
    SyncData *data;
    
    // 这个锁是为了解决多线程中生成syncdata对象时冲突的问题
    spinlock_t lock;

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

// Use multiple parallel lists to decrease contention among unrelated objects.
// 使用多个列表减少不相关对象的竞争
// 通过宏直接获取列表中关联对象的SyncList的data和lock数据
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

syncDataLists是StripedMap泛型字典, 该字典在iOS系统中默认是8个元素, Mac OS中是64个

一句话总结就是: synchronized通过对objec的封装, 关联了对应的lock结构, 通过lock实现同步锁,

回到开头的四个问题

  1. 锁是如何与你传入 @synchronized 的对象关联上的

    通过对象地址关联的, 即任何存在内存地址的对象都可以作为synchronize的key使用

  2. 是否会对关联的对象有强引用

    没有强引用, 只是将内存地址作为key使用

    2022.3.14 更正:

    通过 clang 查看编译后的内容, 存在id _sync_obj = (id)self;语句,如果是 ARC 下这里会造成 retain 操作,通过汇编也可以看到调用了 objc_retain, 后续的objc_sync_enter(_sync_obj);内是没有强引用操作的。所以严格来说ARC 下是会有强引用的。MRC 环境下没有强引用。

  3. 如果synchronize传入nil会有什么问题

    通过entry源码发现, 传入nil 会调用objc_sync_nil, 而BREAKPOINT_FUNCTION 对该函数的定义为asm()"")即空汇编指令, 不会做任何事情

  4. 假如你传入 @synchronized 的对象在 @synchronized 的 block 里面被释放或者被赋值为 nil 将会怎么样

    被释放后, 不会执行任何代码, 此时的锁也没有被释放,即一直处于锁定状态

  5. synchronized是否是可重入的,即是否为递归锁 是可递归的, 因为内部是对os_unfair_recursive_lock的封装, os_unfair_recursive_lock结构通过os_unfair_lock和count实现了可递归的功能

使用注意事项

慎用@synchronized(self)

为了避免该对象在外部也被作为synchronized的key使用, 造成死锁的状况, 所以尽量不使用self作为key, 应该使用外部不可见的私有变量作为key, 以下示例会造成synchronized和_sharedLock的死锁

//class A
@synchronized (self) {
    [_sharedLock lock];
    NSLog(@"code in class A");
    [_sharedLock unlock];
}

//class B
[_sharedLock lock];
@synchronized (objectA) {
    NSLog(@"code in class B");
}
[_sharedLock unlock];

精准的粒度控制

通过源码可以看到, synchronized相比其他锁只是多了查找过程, 性能效率不会过低, 之所以慢是更多的因为没有做好粒度控制

例如以下示例

@synchronized (sharedToken) {
    [arrA addObject:obj];
}

@synchronized (sharedToken) {
    [arrB addObject:obj];
}

使用同一个token来同步arrA和arrB的访问,虽然arrA和arrB之间没有任何联系。传入self的就更不对了。

应该是不同的数据使用不同的锁,尽量将粒度控制在最细的程度。上述代码应该是:

@synchronized (tokenA) {
    [arrA addObject:obj];
}

@synchronized (tokenB) {
    [arrB addObject:obj];
}

另外因为锁执行时会锁定当前代码块, 所以应该避免内部调用函数, 其他开发者可能并不知道函数链上的函数处于锁定状态

参考

关于 @synchronized,这儿比你想知道的还要多-杨萧玉

正确使用多线程同步锁@synchronized()