使用 @synchronized (递归互斥锁)
Objective-C 代码中动态创建互斥锁的便捷方式。该 @synchronized
指令执行任何其他互斥锁会执行的操作——它防止不同的线程同时获取相同的锁。但是,在这种情况下,您不必直接创建互斥体或锁定对象。相反,只需使用任何 Objective-C 对象作为锁定令牌,如下例所示:
- (void)setObj:(id)anObj{
@synchronized (锁对象) {
// 大括号之间的所有内容都受到@synchronized 指令的保护。
}
}
传递给 @synchronized
指令的对象是用于区分受保护块的唯一标识符。如果在两个不同的线程中执行上述方法,并在每个线程上为参数传递不同的对象,则每个线程都将获取其锁定并继续处理而不会被另一个线程阻塞。但是,如果在两种情况下都传递相同的对象,则其中一个线程将首先获取锁,而另一个线程将阻塞,直到第一个线程完成临界区。
@synchronized
这个结构发挥了和锁一样的作用,那么它是如何保证数据安全的呢?
编译 @synchronized
通过 Clang 编译 可以看到 @synchronized 块会转化成一对 objc_sync_enter
和 objc_sync_exit
的函数调用,代码如下:
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;
}
通过对 objc_sync_enter
方法符号断点,查看底层所在的源码库,通过断点发现在 libobjc.A.dylib
中。
objc_sync_enter & objc_sync_exit
// 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) {
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;
}
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;
}
在 objc4 源码中可以找到 objc_sync_enter
和 objc_sync_exit
函数在 objc-sync
文件中实现如上。
objc_sync_enter
通过 id2data
函数传入 obj
和 ACQUIRE
,获取到 SyncData
类型的 data
变量,通过 data
下的 mutex
成员 调用 lock
函数。
objc_sync_exit
通过 id2data
函数传入 obj
和 RELEASE
,获取到 SyncData
类型的 data
变量,通过 data
下的 mutex
成员 调用 tryUnlock
函数。
那么 SyncData
是什么呢? id2data
函数又做了什么呢?
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;
首先,在 objc-sync
文件顶部看到了结构体 struct SyncData
的定义。
- nextData
每个SyncData
含有一个指向其他SyncData
的指针nextData
。因此,可以认为每个SyncData
都是链表里的一个节点。 - object
这个结构体包含了一个object
(传入@synchronized的对象) - threadCount
每个SyncData含有一个threadCount
来表示在使用或者等待锁的线程的数量。这很有用,因为SyncData
是被缓存的,当threadCount
== 0时,表示一个SyncData
的实例能被复用。 - mutex
一个关联着这个锁以及被锁对象的recursive_mutex_t
类型。
SyncCacheItem
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;
接着,是 struct SyncCacheItem
和 struct SyncCache
的定义,SyncCacheItem 中存储了 一个 SyncData
指针,以及 lookCount
,lookCount
是此线程锁定此块的次数,
SyncCache
中存储了一个 SyncCacheItem
的 list
数组,以及当前已使用的个数 used
和 被创建的个数 allocated
。
SyncList
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
static StripedMap<SyncList> sDataLists;
最后是 struct SyncList
的定义。可以把一个 SyncData
当作链表中的一个节点。每个 SyncList
结构都有一个指向 SyncData
链表头部的指针,就像一个用于避免多线程并发的修改该链表的锁一样。
这个代码块的最后一行是一个 sDataLists
的定义,这个 sDataList
数组是一个哈希表,用于把 Objectice-C 对象映射到他们对应的锁。
id2data
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
//在 DataLists 根据 obj 找到 对应 SyncList
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
//在线程存储空间中根据 SYNC_DATA_DIRECT_KEY 查找 data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
// 如果查找到
if (data) {
fastCacheOccupied = YES;
//判断 object 若相同:
//1. result = data
//2. 根据 SYNC_COUNT_DIRECT_KEY 获取当前线程存储空间(tls)存储的 lookCount
//3. 如果是 ACQUIRE,则 lookCount + 1,在存入 线程存储空间(tls)中
//4. 如果是 Release,则 lookCount - 1,在存入 线程存储空间(tls)中,
// 如果 lookCount = 0 ,则 从当前线程存储空间中移除,并对 threadCount - 1
//5. 返回 SyncData 类型 result
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");
}
switch(why) {
case ACQUIRE: {
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;
}
return result;
}
}
#endif
//查找全局 Cache
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
//当前 Cache 存在
if (cache) {
unsigned int i;
//在 Cache 的 List 中根据objcect 查找
//若 object 相同,则操作同上
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();
{
//在 SyncList 中查找
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
//如果找到了,则 threadCount + 1
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
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;
//没有与当前对象关联的SyncData
// an unused one was found, use it
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.
//创建初始化
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
{
//存入 Cache
// 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
方法中,主要分为三种情况:
在
tls
中找到缓存,说明在同一个线程,根据ACQUIRE
或RELEASE
动作,对tls
中的lookCount
做 + 1 或 - 1操作,如果是RELEASE
,lookCount = 0
的情况下,则从当前线程移除,并对threadCount -1
在 Cache 中找到缓存,说明不在同一个线程,根据
ACQUIRE
或RELEASE
动作, 对item
的lookCount
做 + 1 或 - 1操作,如果是ACQUIRE
,对SyncData
的threadCount + 1
;如果是RELEASE
,lookCount = 0
的情况下,则从Cache
移除,并对threadCount -1
未找到缓存,如果为
ACQUIRE
,则新建初始化,threadCount = 1
,lockCount = 1
,存储到tls
和Cache
中;如果是RELEASE
,则返回 nil。
objc_sync_nil
当传入 nil
的时候他们什么都不会做。如果往 @synchronized
里传入 nil
,那么相当于并没有进行过加锁操作,同时代码将不再是线程安全的了!先确保没有把 nil 传入 @synchronized
代码块。可以通过给 objc_sync_nil
设置一个符号断点来检查,objc_sync_nil
是一个空方法,会在往 objc_sync_enter
传入 nil
的时候调用,这会让调试方便的多。
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
总结
- 对于每个加了同步的对象,Objective-C 的运行时都会给其分配一个递归互斥锁,并且保存在一个哈希表中;
-
@synchronized
可嵌套,主要是由于lockCount
和threadCount
的搭配; - 由于底层中链表查询、缓存的查找以及递归,是非常耗内存以及性能的,所以
@synchronized
性能较低,因此在并发较高时,建议使用其他锁; - 注意不要往
@synchronized
代码块中传入nil
!这会毁掉代码的线程安全性。通过往objc_sync_nil
加入符号断点可以看到这种情况的发生。