synchronized实现原理及缺陷分析
@synchronized
是 Objective-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;
sDataLists
是 StripedMap<SyncList>
类型的一个静态变量。
其中 StripedMap
为一个最大可以存储64个变量的字典,LOCK_FOR_OBJ(obj)
和 LIST_FOR_OBJ(obj)
两个宏可以根据 obj
的内存地址来获取对应的 SyncList
中的 data
和 lock
。
5. _objc_pthread_data
在iOS中,每个线程都维护一个 _objc_pthread_data
的结构体,该结构体下维护一个 SyncCache
,该 SyncCache
初始大小为 4
个 SyncData
大小,当 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.data
和 SyncCacheItem.lockCount
进行读取与设置。
另外, _objc_pthread_data
其实也是保存在 tls
中的,它对应的读取关键字为 _objc_pthread_key
。
以上几个数据结构及相关概念就是 @synchronized
实现加锁解锁操作所涉及到的内容,以上内容可能较为负责,可以通过下面这张图来理解:
看完了相关的数据结构以及知识,接下来我们可以探究 objc_sync_enter
和 objc_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_enter
和 objc_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;
}
代码较多,但是流程简单,大致流程如下图所示:
以上就是根据 @synchronized
提供的变量来获取对应 SyncData
的流程,从流程中可知,当变量被加锁后,会生成对应的 SyncData
,并被全局 StripedMap<SyncList>
以及 线程快速缓存
/ 线程SyncCache
持有。
五、使用时注意事项及缺陷
既然我们了解了 @synchroized
的运行原理,那么从中可以总结一些注意事项。
1. 是否可以使用非OC对象作为加锁条件
答案是不可以。
我们从 clang
转换之后的代码可知,@synchronized
第一步就是将加锁条件进行强引用给 id
类型的 _sync_objc
变量,所以此处不接受非OC对象作为加锁条件。
同时我们从 id2data
方法接收参数为 id
类型也能推断出不能接受非OC对象作为加锁条件。
2. 加锁条件为nil时会发生什么
从 objc_sync_enter
和 objc_sync_exit
实现可知,当加锁条件为nil时,临界区代码正常执行,但无法加锁解锁,不能保证临界区代码在线程中的安全。
3. @synchronized为何要对加锁条件进行强引用
@synchronized会对加锁条件进行强引用,这是因为第一步就是进行 id _sync_obj = (id)加锁条件
的操作,但为何要进行一次引用呢?
原因在于若不进行引用,直接对加锁条件进行操作,那么如果在临界区中对加锁条件进行改变,那么在后续的 objc_sync_exit
中获取到的 SyncData
就会发生变化,最终导致加锁解锁操作不对称。
4. 既然@synchronized对加锁条件进行了强引用保护,那么是否可以在临界区代码中对加锁条件进行更改
不建议在临界区代码中对加锁条件进行更改的操作。
原因在于若在临界区代码中对加锁条件进行更改,那么此时如果再次对该加锁条件进行加锁,此时获取的 SyncData
为不同对象对应的值,虽说也能成功加锁,但是无法保证与第一次加锁线程互斥,可能造成业务逻辑的错误。
5. 是否可以对所有需要锁的操作都使用同一个加锁条件
不建议对所有需要锁的操作使用同一个加锁条件。
原因在于当某个操作对加锁条件进行加锁后,若其他与该操作无关的操作再对加锁条件进行加锁时,需等到前一个操作执行完毕,这可能造成无关操作多余无用的等待时间,造成程序效率低下。
所以建议对涉及共同资源的操作使用同一个加锁条件进行加锁,相互无关的操作使用不同的加锁条件加锁。
以上就是该篇博客的全部内容,当我们对 @synchronized
的运行原理有足够了解后,就能够更加合理的使用它,并且正确的避开相关缺陷。