iOS线程安全最常用的锁 - @synchronized

前篇文章整理了iOS中常见的几种锁,我最常用的是@synchronized,接下来我们来一起学习下其底层原理

@synchronized是如何实现递归互斥的?是如何实现可重入的呢?带着这两个问题去分析源码。

我们先用clang命令查看@synchronized的.cpp中的实现

#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *syObject = [NSObject alloc];
        @synchronized (syObject) {
        }
    }
    return NSApplicationMain(argc, argv);
}
NSObject *syObject = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
{ 
    id _rethrow = 0; 
    id _sync_obj = (id)syObject;
    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);}
}

编译后的代码分析出,先去调用了objc_sync_enter然后是一个_SYNC_EXIT的构造函数和析构函数,构造函数什么都没做,析构函数出了作用域就会调用的objc_sync_exit@synchronized会被编译成objc_sync_enterobjc_sync_exit

接下来我们通过源码查看他们都做了什么

objc_sync_enter

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;
}

objc_sync_exit

// 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;
}

objc_sync_enterobjc_sync_exit都是先判定参数objnil就啥也不做,obj不为nil通过id2data获得一个SyncData的数据结构,并且通过id2data的第二个参数来区分是enter调用的id2data还是exit调用的id2dataSyncData里拿出递归锁加锁解锁操作。

先了解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是单向链表结构,为每一个@synchronized 的参数object分配了一把递归锁和记录线程数量。这两个分配记录就是多线程下递归调用的根本。(@synchronized(objc1) 相当于是SyncData->object=objc1

using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
class recursive_mutex_tt : nocopy_t {
    os_unfair_recursive_lock mLock;
    ......
}

OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY
typedef struct os_unfair_recursive_lock_s {
    os_unfair_lock ourl_lock;
    uint32_t ourl_count;
} os_unfair_recursive_lock, *os_unfair_recursive_lock_t;

可以看到,其根本是基于os_unfair_lock的封装。在之前的版本,这个是基于pthread_mutex_t的封装。

关于这个锁可以继续看其定义:

我们现在要关注的是SyncData里的成员是如何在多线程下实现递归调用的。关键逻辑还得看id2data里面做了什么。

static SyncData* id2data(id object, enum usage why) {
    // 从全局hash表中, 通过object获取锁
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    // 从全局hash表中, 通过object获取指向SyncData单向链表的头指针
    SyncData **listp = &LIST_FOR_OBJ(object);
    // 查询后需要返回的结果
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS
    /* 快缓存。 : 
    2个固定的线程键 储存一个单独的 SyncCacheItem 
    Fast cache: two fixed pthread keys store a single SyncCacheItem. 
    这就避免了对于一次只同步单个对象的线程使用SyncCache的malloc 
    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 
    */
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //拿出快速缓存里面的SyncData
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    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");
            }
            //加锁的时候(ENTER)
            switch(why) {
            case ACQUIRE: {
                lockCount++;
                //lockCount放入快速缓存
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            //解锁的时候(EXIT)
            case RELEASE:
                lockCount--;
                //取出加锁的时候的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
                    // SyncData中记录线程数量的-1
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }
            //返回
            return result;
        }
    }
#endif

    /*
        SyncCache 查找
        在线程的TLS中找objc对象,然后再维护2个count lockCount 和 threadCount
        每个线程都只有一份
    */
    // 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;
            // 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--;
                //如果==0,该线程已经使用完了
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    // threadCount -1。防止和加锁的时候通途
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }
            return result;
        }
    }

    /*
        sDataLists 查找
        这里加锁内容包括sDataLists查找,和创建SyncData,目的是为了防止创建重复的SyncData
    */
    // 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;
        //遍历链表
        for (p = *listp; p != NULL; p = p->nextData) {
            //找到SyncData
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                // threadCount + 1
                OSAtomicIncrement32Barrier(&result->threadCount);
                // 跳转:done
                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
        //利用链表里面的无用节点
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
    /*
        创建SyncData
    */
    // 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.
    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;
}

TLS快速缓存 查找

行数很多,可以分为几个部分,第一部分快速缓存查找:

1.TLS快速缓存中只存储了一个SyncData数据,从这里取出的SyncDataobject@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData
2.如果找到了SyncData,对lockCountthreadCount做记录更新,直接把SyncData返回出去;
3.如果没有找到SyncData,则进入下一部分。

注意⚠️TLS(thread Local Store)为线程本地存储。也就是说每条线程都会有一个这样的FastCache。并不是整个过程只有一个FastCache。如果在FastCache找到就直接返回。

syncCache 查找

1.遍历带锁的每个线程的缓存,取出每一个SyncCacheItem,取出SyncCacheItem里面的SyncDataSyncDataobject@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData
2.如果找到了SyncData,对lockCountthreadCount做记录更新,直接把SyncData返回出去;
3.如果没有找到SyncData,则进入第三部分。

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里面是数组,这里遍历查找。可以看其中fetch_cache(NO)中的代码:

static SyncCache *fetch_cache(bool create) {
    _objc_pthread_data *data; 
    data = _objc_fetch_pthread_data(create); 
    if (!data) return NULL; 
    if (!data->syncCache) { 
        if (!create) { return NULL; } 
        else {
            int count = 4; 
            data->syncCache = (SyncCache *) calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem)); 
            data->syncCache->allocated = count; 
        } 
    } // Make sure there's at least one open slot in the list. 
    if (data->syncCache->allocated == data->syncCache->used) { 
        data->syncCache->allocated *= 2; 
        data->syncCache = (SyncCache *) realloc(data->syncCache, sizeof(SyncCache) + data->syncCache->allocated * sizeof(SyncCacheItem));
    } 
    return data->syncCache; 
}

其中_objc_pthread_data结构如下:

typedef struct {
    struct _objc_initializing_classes *initializingClasses; // for +initialize
    struct SyncCache *syncCache;  // for @synchronize
    struct alt_handler_list *handlerList;  // for exception alt handlers
    char *printableNames[4];  // temporary demangled names for logging
    const char **classNameLookups;  // for objc_getClass() hooks
    unsigned classNameLookupsAllocated;
    unsigned classNameLookupsUsed;
    // If you add new fields here, don't forget to update 
    // _objc_pthread_destroyspecific()
} _objc_pthread_data;

这里也进一步说明了TLSsyncCache也是每个线程中都存在一份的。 很明显线程缓存保存了好多的SyncData+lockCount

sDataLists 查找 或 创建SyncData

如果在快速缓存和缓存里面都没有找到,这时候是这个线程第一次走到 @synchronized的地方,系统会去sDataLists里面去找对应的SyncData对象.sDataLists全局Hash表,在id2data函数一开头就获取了全局Hash表的元素

// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
    struct PaddedT {
        T value alignas(CacheLineSize);
    };
    PaddedT array[StripeCount];
    ......
}

struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

sDataListsStripedMap类型,StripedMap存储的是 真机下8张表/模拟器下64张表,每张表里存储的是很多的SyncList = SyncData单向链表 + lock。这里也说明了一个问题,为什么@synchronized在模拟器中的性能会很差,因为在模拟器中会从64张表中去查找锁,而真机是从8张表中查找锁.

1.遍历全局HashStripedMap,取出的SyncData单向链表object@synchronized 的参数object做对比(相同则说明是我们要找到的SyncData),如果对比不是同一个,会找链表下一个元素对比。
2.当前没有与object关联的SyncData,则直接返回nil
3.找到一个没用过的SyncData,就对其缓存到TLS快速缓存线程缓存,并返回这个SyncData 4.如果是TLS快速缓存线程缓存全局Hash表StripedMap都没有找到,说明object被第一次加锁,去创建一个SyncData 返回它。

缓存到线程中

在sDataLists 查找 及 创建SyncData后会调用done,这里只会在Enter的时候执行,如果支持快速缓存并且快速缓存里面没有值,那么在快速缓存里面去添加,方便下次递归的时候来加锁。否则就在线程缓存里面添加。

id2data在找锁的过程中使用了类似三级缓存的流程,这样的目的是为了在多线程中管理锁,并且让线程以最快的速度拿到锁,来完成加锁解锁的操作,从而提升效率。

总结

  • 在快速缓存中没有找到
  • 在线程缓存中也没有找到
  • 在全局的sDataLists中也没有找到
  • 那就新建一个SyncData

WX20220707-094714@2x.png

猜你喜欢

转载自juejin.im/post/7117441321270247438