synchronized实现原理及缺陷分析

@synchronizedObjective-C中提供的一个用来快速加锁操作的关键字,该篇文章就深度分析一下该关键字的实现原理,并从中找出一些使用中的注意实现以及使用缺陷。

一、使用方式

@synchronized关键字的使用十分简单,如下:

- (void)testSynchronized {
    @synchronized (self) {
        NSLog(@"call test synchronized");
    }
}

仅需一个关键字包围并提供一个用来加锁的变量,就能完成对临界区代码的加锁操作,简直无法更加便利快捷。

那么 @synchronized是如何实现加锁操作的呢?我们来进行进一步的分析。

二、实现方式

我们可以通过 clang来将上述代码转换为具体实现源码:

static void _I_CustomObject_testSynchronized(CustomObject * self, SEL _cmd) {
    {
        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);
            
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_p3_pyrv2p4j0gn_yqv6994w1ryr0000gn_T_CustomObject_77509d_mi_0);
        } 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);}
    }
}

从上述转换出的源码来看,该关键字将提供的用来加锁的变量赋值给 _sync_obj变量,同时调用 objc_sync_enter方法,在此之后执行临界区代码,执行临界区代码后,由一个 _SYNC_EXIT结构体类型的保存有 _sync_obj变量的结构体变量的销毁方法调用 objc_sync_exit方法;在此之后,若有异常抛出,由一个 _FIN结构体类型的变量来处理抛出的异常。

由上述流程不难看出,临界区代码是在一对 objc_sync_enter方法和 objc_sync_exit方法之间执行的,所以 objc_sync_enter方法完成的是加锁操作, objc_sync_exit方法完成的是解锁操作。

这两个方法是如何运行的呢?在开始探索这两个方法实现原理之前,先介绍一下相关的数据结构以及知识。

三、相关数据结构及知识

1. SyncData

typedef struct SyncData {
    struct SyncData* nextData;          //指向下一个SyncData
    DisguisedPtr<objc_object> object;   //当前加锁的对象
    int32_t threadCount;                //使用该对象进行加锁的线程数
    recursive_mutex_t mutex;            //用于加锁的递归锁
} SyncData;

该数据结构为 @synchronized实现原理中最基本的数据结构,其中记录了提供的用于加锁的变量,使用该变量加锁的线程数以及与该变量一一对应的一个锁。

2. SyncCacheItem

typedef struct {
    SyncData *data;             //该缓存条目对应的SyncData
    unsigned int lockCount;     //该对象在该线程中被加锁的次数
} SyncCacheItem;

该数据结构用来记录某个 SyncData在某个线程中被加锁的记录,由定义可知,一个 SyncData可以被多个 SyncCacheItem持有。

3. SyncCache

typedef struct SyncCache {
    unsigned int allocated;     //该缓存此时对应的缓存大小
    unsigned int used;          //该缓存此时对应的已使用缓存大小
    SyncCacheItem list[0];      //SyncCacheItem数组
} SyncCache;

该数据结构用来记录某个线程中所有 SyncCacheItem,并且记录了缓存大小以及已使用缓存大小。

4. StripedMap<SyncList>

struct SyncList {
    SyncData *data;     //SyncData数组
    spinlock_t lock;    //自旋锁

    SyncList() : data(nil) { }
};

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

sDataListsStripedMap<SyncList>类型的一个静态变量。

其中 StripedMap为一个最大可以存储64个变量的字典,LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj)两个宏可以根据 obj的内存地址来获取对应的 SyncList中的 datalock

5. _objc_pthread_data

在iOS中,每个线程都维护一个 _objc_pthread_data的结构体,该结构体下维护一个 SyncCache,该 SyncCache初始大小为 4SyncData大小,当 SyncCache缓存填满时,会以上次大小的 2倍进行扩充。

6. TLS

TLS全称为 Thread Local Storage,在iOS中,每个线程都拥有自己的 TLS,负责保存本线程的一些变量, TLS无需锁保护。

tls_get_direct/ tls_set_direct提供了快速从当前线程获取/设置对应变量的方法。

iOS中内设了两个宏,SYNC_DATA_DIRECT_KEY/ SYNC_COUNT_DIRECT_KEY,它们的用是与tsl_get_direct/ tls_set_direct配合,分别对 SyncCacheItem.dataSyncCacheItem.lockCount进行读取与设置。

另外, _objc_pthread_data其实也是保存在 tls中的,它对应的读取关键字为 _objc_pthread_key

以上几个数据结构及相关概念就是 @synchronized实现加锁解锁操作所涉及到的内容,以上内容可能较为负责,可以通过下面这张图来理解:

synchronized数据结构

看完了相关的数据结构以及知识,接下来我们可以探究 objc_sync_enterobjc_sync_exit的实现了。

四、内部实现

接下来的源码都保存在runtime源码中。

1. objc_sync_enter

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

    if (obj) {
        // 通过id2data获取SyncData
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        // 将SyncData的锁进行加锁
        data->mutex.lock();
    } else {
        // 处理传入obj为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;
}

2. objc_sync_exit

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        // 通过id2data获取SyncData
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            // 将SyncData的锁进行解锁
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // 处理传入obj为nil情况
        // @synchronized(nil) does nothing
    }
	

    return result;
}

objc_sync_enterobjc_sync_exit的实现很简单,都是通过 id2data方法获取到对应的 SyncData对象,进而对该对象的递归锁进行加锁解锁。

那么 id2data实现原理如何呢?

3. id2data

static SyncData* id2data(id object, enum usage why)
{
    //获取所需加锁对象在StripedMap类型全局变量sDataLists中所对应的自旋锁及SyncData数组
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    
    //初始化所需加锁对象最终对应的SyncData对象
    SyncData* result = NULL;

    //SUPPORT_DIRECT_THREAD_KEYS表示可以使用快速缓存
    //tls_get_direct/tls_set_direct是从tls(Thread Local Storage)线程局部存储中获取变量
    //快速缓存的含义为:定义两个变量SYNC_DATA_DIRECT_KEY/SYNC_COUNT_DIRECT_KEY,可以从线程局部缓存中快速取得SyncCacheItem.data和SyncCacheItem.lockCount
    //该缓存策略可以避免线程只对一个对象进行加锁时创建SyncCache的多余消耗
#if SUPPORT_DIRECT_THREAD_KEYS
    //标识是否已经有快速缓存了
    bool fastCacheOccupied = NO;
    //获取快速缓存中的SyncData
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        //将标识置为YES
        fastCacheOccupied = YES;

        //判断快速缓存中的SyncData是否为所需加锁对象对应的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);
            
            //检测该SyncData是否合法
            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减少并更新
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                //若该对象此时没有任何线程加锁,则从快速缓存中移除,并减少SyncData所被线程使用的个数
                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,若不存在,不需要新建
    SyncCache *cache = fetch_cache(NO);
    //若线程下的SyncCache已经被创建
    if (cache) {
        unsigned int i;
        //寻找所需加锁对象对应的SyncData在该线程SyncCache中是否存在,若存在则做相应处理
        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;
        }
    }

    //线程对应的SyncCache不存在,或在线程对应的SyncCache中没有找到所需加锁对象对应的SyncData
    
    //将所需加锁对象在全局存储中所处SyncList对应的lock加锁,为从SyncList中搜索对应SyncData做准备
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //遍历SyncList中SyncData数组
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                //从SyncData数组中找到对应SyncData,由于此时是线程中SyncCache中未找到SyncData,说明该SyncData为第一次使用在线程中
                //此时需要将threadCount增加,并进入最后处理
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //寻找SyncData数组中未使用的SyncData并赋予firstUnused
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        //若对应操作为RELEASE或CHECK,则直接进入最后处理
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        //将SyncData数据置为当前objc,同时将该SyncData存入全局存储中,并进入最终处理
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    //至此,线程SyncCache不存在或该线程SyncCache中不存在SyncData,且全局SyncList中也未找到SyncData,说明该对象对于全部线程来说是第一次加锁
    //为该对象创建对应的SyncData
    result = (SyncData*)calloc(sizeof(SyncData), 1);
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t();
    //将该SyncData存入全局对应的SyncList中
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        //进入此处的情况为该线程第一次使用该对象
        
        if (why == RELEASE) {
            //此时为该线程第一次使用该对象,若第一次就为RELEASE,则不需要做任何处理
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        //若支持快速缓存,并且线程局部存储中没有存储其余SyncCacheItem,则将该SyncCacheItem数据写入快速缓存中
        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
        {
        //若不支持快速缓存或快速缓存已存有数据,则将该SyncCacheItem存入该线程对应的SyncCache中
            // 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;
}

代码较多,但是流程简单,大致流程如下图所示:

id2data方法流程图

以上就是根据 @synchronized提供的变量来获取对应 SyncData的流程,从流程中可知,当变量被加锁后,会生成对应的 SyncData,并被全局 StripedMap<SyncList>以及 线程快速缓存/ 线程SyncCache持有。

五、使用时注意事项及缺陷

既然我们了解了 @synchroized的运行原理,那么从中可以总结一些注意事项。

1. 是否可以使用非OC对象作为加锁条件

答案是不可以。

我们从 clang转换之后的代码可知,@synchronized第一步就是将加锁条件进行强引用给 id类型的 _sync_objc变量,所以此处不接受非OC对象作为加锁条件。

同时我们从 id2data方法接收参数为 id类型也能推断出不能接受非OC对象作为加锁条件。

2. 加锁条件为nil时会发生什么

objc_sync_enterobjc_sync_exit实现可知,当加锁条件为nil时,临界区代码正常执行,但无法加锁解锁,不能保证临界区代码在线程中的安全。

3. @synchronized为何要对加锁条件进行强引用

@synchronized会对加锁条件进行强引用,这是因为第一步就是进行 id _sync_obj = (id)加锁条件的操作,但为何要进行一次引用呢?

原因在于若不进行引用,直接对加锁条件进行操作,那么如果在临界区中对加锁条件进行改变,那么在后续的 objc_sync_exit中获取到的 SyncData就会发生变化,最终导致加锁解锁操作不对称。

4. 既然@synchronized对加锁条件进行了强引用保护,那么是否可以在临界区代码中对加锁条件进行更改

不建议在临界区代码中对加锁条件进行更改的操作。

原因在于若在临界区代码中对加锁条件进行更改,那么此时如果再次对该加锁条件进行加锁,此时获取的 SyncData为不同对象对应的值,虽说也能成功加锁,但是无法保证与第一次加锁线程互斥,可能造成业务逻辑的错误。

5. 是否可以对所有需要锁的操作都使用同一个加锁条件

不建议对所有需要锁的操作使用同一个加锁条件。

原因在于当某个操作对加锁条件进行加锁后,若其他与该操作无关的操作再对加锁条件进行加锁时,需等到前一个操作执行完毕,这可能造成无关操作多余无用的等待时间,造成程序效率低下。

所以建议对涉及共同资源的操作使用同一个加锁条件进行加锁,相互无关的操作使用不同的加锁条件加锁。

以上就是该篇博客的全部内容,当我们对 @synchronized的运行原理有足够了解后,就能够更加合理的使用它,并且正确的避开相关缺陷。

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/88399115