美文网首页iOS-机制
iOS-锁-@synchronized

iOS-锁-@synchronized

作者: xxxxxxxx_123 | 来源:发表于2020-03-13 16:54 被阅读0次

    @synchronized,同步锁,又名对象锁,由于其使用简单,基本上是在iOS开发中使用最频繁的锁。

    使用方式如下:

    @synchronized() {
        // 需要加锁的代码块
    }
    

    原理

    那么@synchronized到底是如何实现了锁的功能呢?我们看一个例子:

    - (void)synchronizedTest {
        @synchronized (self) {
            NSLog(@"====synchronized====");
        }
    }
    

    对程序设置一个断点,进入汇编,我们可以看到,发生变化的前后包裹了两个方法:

    image
    objc_sync_enter
    objc_sync_exit
    

    或者使用下面代码对程序进行编译:

    clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
    
    image

    也可以得出,分析的重点应该是以下代码:

    objc_sync_enter
    objc_sync_exit
    

    通过符号断点我们可以将上述代码定位到objc源码。

    // Allocates recursive mutex associated with 'obj' if needed.
    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            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;
    }
    
    BREAKPOINT_FUNCTION(
        void objc_sync_nil(void)
    );
    

    从代码可以得出以下结论:

    • @synchronized使用的是递归锁(recursive mutex)
    • @synchronized(nil)不会做任何事情,可以用来防止死递归。

    我们再来看看当obj存在的时候,@synchronized做了什么。

    SyncData* data = id2data(obj, ACQUIRE);
    

    通过这行代码,可以看出来obj是以SyncData这种结构来保存的。SyncData是一个结构体,具体信息如下:

    typedef struct alignas(CacheLineSize) SyncData {
        struct SyncData* nextData;
        DisguisedPtr<objc_object> object;
        int32_t threadCount; 
        recursive_mutex_t mutex;
    } SyncData;
    
    • struct SyncData* nextDataSyncData的指针节点,指向下一条数据
    • DisguisedPtr<objc_object> object:锁住的对象
    • int32_t threadCount:等待的线程数量
    • recursive_mutex_t mutex:使用的递归锁

    获取SyncData结构的数据的流程是怎样的?

    1. 如果支持tls缓存,从tls缓存获取obj的相关信息。该方法是检查每个线程单项快速缓存中是否有匹配的对象。
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    

    此处引入一个概念,tlsThread Local Storage,线程局部存储,它是操作系统为线程单独提供的私有空间,通常只有有限的容量。

    result = data;
    lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
    lockCount++;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    

    此处如果多次进入,也就是递归操作,只会对lockCount进行加1操作。

    如果获取到数据,说明对象又被加了一次锁,更新tls中存储的obj信息,锁的次数加1,并将数据返回。如果没有获取到,则进入第二步。

    1. 在线程缓存SyncCache中查找是否存在obj的数据信息。该方法是检查已拥有锁的每个线程高速缓存中是否有匹配的对象。
    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 *cache = fetch_cache(NO);
    SyncCacheItem *item = &cache->list[i];
    item->lockCount++;
    

    如果存在当前obj的数据信息,将线程缓存SyncCache中的obj的锁的次数加1,并将数据返回。如果没找到就进入第3步。

    1. 在使用列表sDataLists中查找对象

    在列表sDataLists中查找,需要对查找过程加锁,防止在多线程查找导致的异常。使用列表sDataListsSyncData又做了一层封装,元素是一个结构体SyncList

    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    
    using spinlock_t = mutex_tt<LOCKDEBUG>;
    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    struct SyncList {
        SyncData *data;
        spinlock_t lock;
    
        constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };
    static StripedMap<SyncList> sDataLists;
    

    遍历,进行匹配:

    SyncData* p;
    SyncData* firstUnused = NULL;
    for (p = *listp; p != NULL; p = p->nextData) {
        if ( p->object == object ) {
            result = p;
            OSAtomicIncrement32Barrier(&result->threadCount);
            goto done;
        }
        if ( (firstUnused == NULL) && (p->threadCount == 0) )
            firstUnused = p;
    }
    

    如果找到,就将数据写入tls缓存和线程缓存SyncCache,并返回数据。

    // 写入tls缓存
    tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
    
    // 写入线程缓存
    if (!cache) cache = fetch_cache(YES);
    cache->list[cache->used].data = result;
    cache->list[cache->used].lockCount = 1;
    cache->used++;
    
    1. 创建一个新的SyncData放入sDataLists中,并存入tls缓存和线程缓存中,然后返回。
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    // 从这里可以看出来@synchronized其实是一个递归锁
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    

    看完了获取锁,我们再来看看释放锁。释放的过程和保存相似。如果传入的对象是空的,也不会做任何事情。

    // End synchronizing on '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) {
            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;
    }
    

    如果传入的对象有值:

    1. 先从tls缓存中查找,如果找到,对锁的计数减1,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从tls缓存中删除。
    lockCount--;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    if (lockCount == 0) {
        tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);                    OSAtomicDecrement32Barrier(&result->threadCount);
    }
    
    1. 从线程缓存SyncCache中查找,如果找到,对锁的计数减1,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从线程缓存SyncCache中删除。
    item->lockCount--;
    if (item->lockCount == 0) {
        cache->list[i] = cache->list[--cache->used];                OSAtomicDecrement32Barrier(&result->threadCount);
    }
    
    1. sDataLists查找,找到的话,直接将其置为nil

    其实,@synchronized就是一个递归锁,其内部维护了一张表用来存储对象和锁的相关信息,加锁和释放锁的操作就是对锁的计数进行操作。

    注意点

    使用@synchronzied的需要注意的是

    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.mArray = [NSMutableArray array];
        });
    }
    

    这段代码运行就会崩溃,是因为,我们在不断地创建arraymArray在不断的赋新值,释放旧值,这个时候多线程操作就会可能存在值已经被释放了,而其他线程还在操作,此时就会发生崩溃。此时就需要我们对程序加锁。将上述程序改成如下:

    @synchronized (self.mArray) {
        self.mArray = [NSMutableArray array];
    }
    

    程序依然会崩溃,原因是@synchronized的操作时如果是nil,则什么也不做,则可能会出现锁不住的情况,同样会导致在释放的时候发现值已经变成nil了。那我们应该怎么改呢?

    第一种方式就是使用信号量加锁:

    dispatch_semaphore_wait(_semp, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.mArray = [NSMutableArray array];
        dispatch_semaphore_signal(self.semp);
    });
    

    第二种直接使用NSLock:

    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            self.mArray = [NSMutableArray array];
            [lock unlock];
        });
    }
    

    在平常的开发中我们要慎用@synchronized(self),直接将self传入@synchronized确实是很简单粗暴的方法,但是这样容易导致死锁的出现。原因是因为self很可能会被外部对象访问,被用作key来生成锁。两个公共锁交替使用的场景就容易出现死锁。

    总结

    @synchronized是递归锁,其实是在底层对recursive_mutex_t做了封装和特殊处理。

    @synchronized具备处理递归能力的是lockCount,让其能够处理多线程的是threadCount

    进入代码块的入口是objc_sync_enter(id obj),出口是objc_sync_enter(id obj)

    核心的处理如下:

    • 如果支持tls缓存,就从tls缓存中查找对象锁SyncData,找到对lockCount进行相应的操作
    • 如果不支持tls缓存,或者从tls缓存中未找到,就从线程缓存SyncCache中查找,同样,找到对lockCount进行相应的操作
    • 如果没有缓存命中,就从sDataLists链表中查找,找到之后进行相关的操作,并写入tls缓存和线程缓存SyncCache
    • 都没有找到,就创建一个节点,将对象锁SyncData插入sDataLists,并写入缓存

    释放对象操作类似。

    需要注意的是@synchronized的操作相对其他锁来说对性能消耗比较大,不建议大量使用。另外再某些多线程操作中,@synchronized可能存在锁不住的情况。

    相关文章

      网友评论

        本文标题:iOS-锁-@synchronized

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