前面的文章把iOS中能用的锁的讲解了一遍,其中@synchronized
是一把非常强大的锁,支持多线程的递归调用,而在使用的时候只要把代码写进括号内,不需要管理锁的加锁和解锁,非常方便。这也是其倍受青睐的原因。本文就一层一层揭开@synchronized
的神秘面纱,探究synchronized
的工作原理。
@synchronized源码入口
首先看看@synchronized
的使用:
int object_c_source_m() {
NSObject *obj1 = [[NSObject alloc] init];
@synchronized (obj1) {
// Code 。。。
}
return 0;
}
可以使用clang -rewrit-objc
,将代码重写为c++代码
。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OSSynchornizedOriginFile.m -o OSSynchCPPForIphone.cpp
可以找到@synchronized
对应的代码如下,为了方便阅读,这里将关键代码进行了提取。
id _sync_obj = (id)obj1;
objc_sync_enter(_sync_obj);
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
}
_sync_exit(_sync_obj);
其中_sync_exit(_sync_obj)
是调用_SYNC_EXIT
的构造函数,而~_SYNC_EXIT()
是_SYNC_EXIT
的析构函数,构造出的实例没有使用,在回收的时候就会调用析构函数。所以根本调用的还是objc_sync_exit(sync_exit)
。
@synchronized
在底层其实是分别调用了objc_sync_enter
和objc_sync_exit
。
enter、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;
}
// 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;
}
从源码中我们可以获得2个有用的信息:
- 如果
obj为nil
,相当于什么都没有做 - 需要进一步了解
SyncData
的数据结构,@synchronized
中也是通过互斥锁(data->mutex)
实现的。
数据结构
在分析正式源码之前,必须要知道其中的部分数据结构的定义,这对流程理解非常重要。
SyncData
源码定义:
//alignas 关键字用来声明对齐字节的
typedef struct alignas(CacheLineSize) SyncData {
//链表结构
struct SyncData* nextData;
//@synchronized中传入的对象
DisguisedPtr<objc_object> object;
// 多少个线程使用
int32_t threadCount; // number of THREADS using this block
// 一把递归锁
recursive_mutex_t mutex;
} SyncData;
可以通过注解SyncData
其实是一个单向链表结构,其中存了我们@synchronized
中传入的对象,还有一把锁,还有线程数量。
关于这个锁可以继续看其定义:
using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
class recursive_mutex_tt : nocopy_t {
os_unfair_recursive_lock mLock;
//code ...
}
/*!
* @typedef os_unfair_recursive_lock
*
* @abstract
* Low-level lock that allows waiters to block efficiently on contention.
*
* @discussion
* See os_unfair_lock.
*
*/
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
的封装。
SyncCache
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
是SyncData
的缓存。可以简易的画出其结构图:
Fast cache
/*
快缓存。 : 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
*/
这一段虽然全是注释,但是非常重要,这里引出了一个关键的概念-Fast cache
。这个跟上面的缓存不一样,上面的缓存是存了一个列表,而Fast cache
只存了单个的SyncCacheItem
用两个key
来获取其成员变量data
和 lockCount
。
sDataLists
// 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;
sDataLists
为全局静态,全局只有一份。可以简单的理解为一张hash Map
。
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// StripedMap<T> is a map of void* -> T, sized appropriately
// 作用是友好的缓存锁
// for cache-friendly lock striping.
// 可以直接缓存spinlock_t,也可缓存其他内部含有spin lock 的结构体
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
//真机状态下
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
//其他
#else
enum { StripeCount = 64 };
#endif
// code ...
}
表的大小跟架构环境和是否是真机有关,可以将sDataLists
的结构总结如下:
id2data
有了上面这些数据结构的基础,我们再来看id2data
这个方法就会比较轻松了。
那先看id2data
的实现:
这里我把
id2data
这小二百行代码分为5个步骤。
Fast cache 查找
#if SUPPORT_DIRECT_THREAD_KEYS
// 1⃣️去快速缓存里面找
// 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");
}
switch(why) {
//加锁的时候(ENTER)
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
注意⚠️其中TLS(thread Local Store)
为线程本地存储。也就是说每条线程都会有一个这样的FastCache
。并不是整个过程只有一个FastCache
。如果在FastCache
找到就直接返回。
syncCache 查找
{
// 2⃣️在线程的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;
}
}
}
这里同样是在缓存找,因为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
的数据结构:
//每一个线程的存储
// objc per-thread storage
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;
这里也进一步说明了TLS
,syncCache
也是每个线程中都存在一份的。
sDataLists 查找
如果在快速缓存和缓存里面都没有找到,这时候是这个线程第一次走到
@synchronized
的地方,系统会去sDataLists
里面去找对应的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.
// 这里加锁内容包括sDataLists查找,和创建SyncData,目的是为了防止创建重复的和创建SyncData
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
{
// 4⃣️再找不到,只能创建
// 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;
}
- 在快速缓存中没有找到
- 在线程缓存中也没有找到
- 在全局的sDataLists中也没有找到
- 那就自己动手丰衣足食,自己新建一个。
缓存到线程中
// 5⃣️缓存
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;
}
这里只会在Enter
的时候执行,如果支持快速缓存并且快速缓存里面没有值,那么在快速缓存里面去添加,方便下次递归的时候来加锁。否则就在线程缓存里面添加。
id2data
在找锁的过程中使用了类似三级缓存
的流程,这样的目的是为了在多线程中管理锁,并且让线程以最快的速度拿到锁,来完成加锁解锁的操作,从而提升效率。
总结
@synchronized
使用时如果传入nil
,不能完成加锁,使用时应避免。@synchronized
使用了快速缓存、线程缓存、全局链表方式来使线程更快的拿到锁,以提升效率。@synchronized
内部是基于os_unfair_lock
封装的递归互斥锁。@synchronized
内部在创建锁的时候为了唯一性,使用到spinlock_t(基于os_unfair_lock封装的)
来确保线程安全。