iOS開発-@synchronizedの主成分分析

概要

iOS開発では、ロックの適用が不可欠です。今日は、一般的に使用される@synchronizedの原理について説明します。

目標を探る

@synchronizedを使用する場合、基本的な使用法は次のとおりです。

@synchronized (self) {
    // your code here
}

ここでは@synchronized、パラメータ、コードブロックが続き、これが@synchronized基本的な使用法であることがわかります。次に、問題を調査します。

  • @synchronizedどのパラメータを渡す必要がありますか?nilを渡すとどうなりますか?
  • @synchronizedコードのブロックは正確には何ですか?
  • @synchronizedの実現原理は何ですか?
  • @synchronizedなぜネストできるのか

@synchronizedの予備調査

ソースコードには@synchronizedの直接実装はなく、この方法で基盤となる実装を直接表示することはできません。

1つをファイルにmain.m実装@synchronizedし、xcrunを使用してその方法を調べます。

image.png

コンパイルされたcppファイルでは、システムコードに加えて、@synchronized生成されたコードが赤いボックスに表示されます。見栄えを良くするためにフォーマットを調整します。

image.png

このタスクでは、NSLog印刷を作成しただけで、コンストラクタとデストラクタが定義された_SYNC_EXIT構造体があり、パラメータを受け取るために使用されるid型もあることがわかりました。sync_exit

簡単な情報を削除すると、基本的なプロセスは次のようになります。

image.png

  • _sync_obj@synchronized着信パラメータ
  • 移行objc_sync_enter(_sync_obj);
  • _sync_exit(_sync_obj):パラメータを使用してデストラクタを呼び出すことは、実際には呼び出すことと同じです。objc_sync_exit(_sync_obj);

全体のプロセスは複雑ではありません、私たちは次に勉強しますobjc_sync_enter(_sync_obj);

image.png

着信objが存在する場合は、objビジネス処理を実行します。コメントなどの場合objは、処理しないでください。nil@synchronized(nil) does nothing

同じ関数objc_sync_exit(id obj)でのデータの処理は似ています

image.png

两个函数都对obj进行了一个id2data()的操作,以及在objc_sync_enterdata进行mutex.lock()mutex.tryUnlock()操作

SyncData分析

简单分析一下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;

先来看一下核心重点的id2data()源码中主要包含三个部分

#if SUPPORT_DIRECT_THREAD_KEYS
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {......}

这里SUPPORT_DIRECT_THREAD_KEYS用到了tls(Thread Local Storage)线程局部存储,是操作系统为线程单独提供的私有空间,通常只有有限容量。

 SyncCache *cache = fetch_cache(NO);
 if (cache) {......}

cache中查找,与上一种方式处理逻辑相似,意味着如果支持tls,走第一种方式,如果不支持则走第二种方式。

// 剩余的部分
{......}
done:
......

LOCK_FOR_OBJLIST_FOR_OBJ的宏定义中,有一个sDataList

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists; // 全局哈希表

其中SyncList是一个带有SyncData和lock的结构

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

sDataList的结构,由于是哈希结构,并不会顺序存储,第一个值不一定实在0位置。

(StripeMap<SyncList>) $0 = {
    array = {
        [0] = {
            value = {
                data = nil
                lock = {
                    mLock = (_os_unfaire_lock_opaque = 0)
                }
            }
        },
        ......
        
        [63] = {
            value = {
                data = nil
                lock = {
                    mLock = (_os_unfaire_lock_opaque = 0)
                }
            }
        },
    }
}

SyncListdata即为syncData,那么我们在使用@synchronized的嵌套用法时如果参数都是self,将如何存储,所以SyncData使用链表结构,同一个self中的不同操作可以形成链表,通过同一个self找到所有的操作。

解析

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

1如果支持tls走这里,第一次并不存在
7嵌套的第二层进来的时候,data存在并且支持tls,进入此处的代码
#if SUPPORT_DIRECT_THREAD_KEYS
    // 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) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            8获取对象被加锁了多少次,如果线程数或锁的数量小于等于零,说明对象不存在
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }
            switch(why) {
            9如果是ACQUIRE,对象加锁数+1,并进行存储
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            10.如果是RELEASE,对象加锁数-1,并进行存储
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                11锁为零,对result中的线程数
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    11线程数-1,这里也说明@synchronized是跨线程的,多线程的
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

2如果不持之走这里,第一次进来并不存在cache
    // 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--;
                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();

3第一次过来list为空,why = ACQUIRE,所以也不走这里
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                12如果是同一个对象
                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
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
4第一次走这里,创建一个新的SyncData并加入到list
    // 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);
    5用头插法进行数据排列
    result->nextData = *listp;
    *listp = result;
    
    6对syncData进行存储
 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;
}

syncData的操作有mutex.lock(),多层嵌套会对相同的参数连续加锁,这也是@synchronized的重要特性之一,

总结

  • @synchronized调用时生成了一张哈希表sDataList,其中维护了一个存放SyncList的array结构

  • @synchronized通过objc_sync_enterobjc_sync_exit对数据进行加锁和解锁的操作,其中用到的是递归锁

  • 其中生成的SyncData有两种存储方式:tls,cache

  • 可重入:通过lockcount对对象进行加锁次数的管理,可多次加锁

  • 多线程:通过threadcount维护多线程对对象加锁的情况

  • @synchronized传入的参数应该保证:生命周期大于多线程要操作的数据源,通常传入self(vc)也是因为在创建syncData链时,创建一条即可。

おすすめ

転載: juejin.im/post/7116436253452533773