@synchronized底层原理

520 阅读6分钟

@synchronized是iOS开发中使用的最多的一把锁,今天我们就来探究一下这把锁的底层究竟是怎么实现的。
首先我们准备了一个空工程,在main函数中增加了如下代码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        id obj = [NSUserDefaults standardUserDefaults];
        @synchronized (obj) {
            NSLog(@"");
        }
        @synchronized (obj) {
            NSLog(@"");
        }
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

在第8行,第二个@synchronized的地方添加断点并且进入汇编断点模式;

main函数底层汇编实现

然后我们在第43行添加断点后按住control键点击step into进入下一层汇编,一次没有跳到可以多跳几次(之所以这里会需要两个@synchronized是因为第一次调用锁会调用dyld_stub_binder去绑定,这样就没办法找到objc_sync_enterobjc_sync_exit的符号究竟在什么地方)。

objc_sync_enter所在动态库

从图中我们可以看到objc_sync_enter是在libobjc.A.dylib中,此时我们去Apple Source Code去下载libobjc的源码。
下载到objc的源码后搜索objc_sync_enter,找到objc_sync_enter的源码,如下:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        // 这里就是@synchronized的真正实现啦,先获取了SyncData * data;
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        // 接着调用了data中的mutex锁进行加锁
        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;
}

由上面的代码可以知道,主要操作其实是如何获取data变量。接着我们跳到id2data(obj, ACQUIRE)函数实现中:

static SyncData* id2data(id object, enum usage why)
{
    // 👉 从哈希表(StripedMap<SyncList> sDataLists)中取出了object对应的spinlock_t
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    // 👉 从哈希表(StripedMap<SyncList> sDataLists)中取出了object对应的SyncData *;
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS
    // 👉 从当前线程的tls缓存中取出Object对应的SyncData *;第一次来没有值
    // 👉 第二次相同线程进来可以取到缓存,此时直接对lockCount进行++操作后return缓存
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            printf("----- find data in tls - threadCount: %d - pointer: %p\n", data->threadCount, data);
            // 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++操作
                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;
            }
            
            // 👉 返回result
            return result;
        }
    }
#endif

    // 👉 从cache中取出SyncData *,一般不会进入这个缓存,只有在不支持tls缓存时才会进来
    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            printf("----- find data in cache - threadCount: %d - pointer: %p\n", data->threadCount, (item->data));
            // 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;
        }
    }

    // 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();

    {
        // 👉 判断从StripedMap<SyncList>中能否找到匹配的SyncData *;
        SyncData* p;
        SyncData* firstUnused = NULL;
        // 👉 从listp中取出的SyncData *及其对应链表下的所有SyncData *数据,判断是否有对应缓存的数据
        // 👉 第一次来这里无法取出值,但当此次锁被完全释放后,tls中已经没有缓存
        // 👉 第二次在进来(从不同的线程/过了10秒后在新的地方加锁)用相同的object加锁,此时这里可以取到上次使用过的SyncData *直接使用
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                printf("----- find a data in listp - threadCount: %d - pointer: %p\n", p ? p->threadCount : 0, p);
                result = p;
                // atomic because may collide with concurrent RELEASE
                // 👉 进行threadCount++的操作
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        printf("----- find a empty data point in listp for object: %p - result: %p\n", &object, result);
        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.
    // 👉 第一次进来会在这创建新的SyncData *赋值给result,并将其保存到哈希表(StripedMap<SyncList> sDataLists)中第一个节点
    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;
    printf("----- create a new data for object: %p - result: %p\n", &object, 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
            // 👉 此时在这里将SyncData *保存到当前线程的tls中,这里保存的只是SyncData结构体的指针地址,
            // 👉 所以不论多少个线程进来保存多少份指针地址,最终指向的都是同一个SyncData结构体类型的变量,修改的threadCount都是同一个
            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;
}

根据上面的代码可以知道获取SyncData *的整体流程如下:

  • 1.从当前线程的tls缓存空间取缓存,如果有的话直接在当前线程进行lockCount++操作后返回SyncData *
  • 2.从cache中取缓存,一般不会进来,除非tls不可用
  • 3.从哈希表StripedMap<SyncList> sDataLists中取出缓存,
    • 第一次来这里无法取出值,进入第四步
    • 第二次在进来(从不同的线程/过了10秒后在新的地方加锁)用相同的object加锁,此时这里可以取到上次使用过的SyncData *直接使用,并进行threadCount++的操作
  • 4.新建SyncData *赋值给result,并将其保存到哈希表StripedMap<SyncList> sDataLists
  • 5.如果在当前线程的tls中没有缓存过SyncData *,则将其缓存在当前线程的tls中,并将lockCount初始化为1也缓存在tls
  • 6.如果tls不可用,则将SyncData *和初始化为1lockCount缓存在cache中

至此整个@synchronized加锁时的底层源码就分析清楚了,由于底层实现中保存了加锁线程数量-threadCount当前线程加锁次数-lockCount两个参数,所以才能实现多线程重入的递归锁
解锁的过程和加锁类似,这里就不看源码实现了,感兴趣的小伙伴可以自己看下objc_sync_exit的源码实现。