@synchronized
是iOS开发中使用的最多的一把锁,今天我们就来探究一下这把锁的底层究竟是怎么实现的。
首先我们准备了一个空工程,在main函数中增加了如下代码:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
id obj = [NSUserDefaults standardUserDefaults];
@synchronized (obj) {
NSLog(@"");
}
@synchronized (obj) {
NSLog(@"");
}
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
在第8行,第二个@synchronized
的地方添加断点并且进入汇编断点模式;
然后我们在第43行添加断点后按住control
键点击step into
进入下一层汇编,一次没有跳到可以多跳几次(之所以这里会需要两个@synchronized
是因为第一次调用锁会调用dyld_stub_binder
去绑定,这样就没办法找到objc_sync_enter
和objc_sync_exit
的符号究竟在什么地方)。
从图中我们可以看到objc_sync_enter
是在libobjc.A.dylib
中,此时我们去Apple Source Code去下载libobjc
的源码。
下载到objc的源码后搜索objc_sync_enter
,找到objc_sync_enter
的源码,如下:
// 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) {
// 这里就是@synchronized的真正实现啦,先获取了SyncData * data;
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 接着调用了data中的mutex锁进行加锁
data->mutex.lock();
} else {
// 👇 下面这行注释写明了如果锁的是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;
}
由上面的代码可以知道,主要操作其实是如何获取data
变量。接着我们跳到id2data(obj, ACQUIRE)
函数实现中:
static SyncData* id2data(id object, enum usage why)
{
// 👉 从哈希表(StripedMap<SyncList> sDataLists)中取出了object对应的spinlock_t
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
// 👉 从哈希表(StripedMap<SyncList> sDataLists)中取出了object对应的SyncData *;
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
// 👉 从当前线程的tls缓存中取出Object对应的SyncData *;第一次来没有值
// 👉 第二次相同线程进来可以取到缓存,此时直接对lockCount进行++操作后return缓存
// 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) {
printf("----- find data in tls - threadCount: %d - pointer: %p\n", data->threadCount, data);
// 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++操作
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;
}
// 👉 返回result
return result;
}
}
#endif
// 👉 从cache中取出SyncData *,一般不会进入这个缓存,只有在不支持tls缓存时才会进来
// 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;
printf("----- find data in cache - threadCount: %d - pointer: %p\n", data->threadCount, (item->data));
// 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();
{
// 👉 判断从StripedMap<SyncList>中能否找到匹配的SyncData *;
SyncData* p;
SyncData* firstUnused = NULL;
// 👉 从listp中取出的SyncData *及其对应链表下的所有SyncData *数据,判断是否有对应缓存的数据
// 👉 第一次来这里无法取出值,但当此次锁被完全释放后,tls中已经没有缓存
// 👉 第二次在进来(从不同的线程/过了10秒后在新的地方加锁)用相同的object加锁,此时这里可以取到上次使用过的SyncData *直接使用
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
printf("----- find a data in listp - threadCount: %d - pointer: %p\n", p ? p->threadCount : 0, p);
result = p;
// atomic because may collide with concurrent RELEASE
// 👉 进行threadCount++的操作
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
printf("----- find a empty data point in listp for object: %p - result: %p\n", &object, result);
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.
// 👉 第一次进来会在这创建新的SyncData *赋值给result,并将其保存到哈希表(StripedMap<SyncList> sDataLists)中第一个节点
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;
printf("----- create a new data for object: %p - result: %p\n", &object, 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
// 👉 此时在这里将SyncData *保存到当前线程的tls中,这里保存的只是SyncData结构体的指针地址,
// 👉 所以不论多少个线程进来保存多少份指针地址,最终指向的都是同一个SyncData结构体类型的变量,修改的threadCount都是同一个
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 *
的整体流程如下:
- 1.从当前线程的
tls
缓存空间取缓存,如果有的话直接在当前线程进行lockCount++
操作后返回SyncData *
; - 2.从
cache
中取缓存,一般不会进来,除非tls
不可用 - 3.从哈希表
StripedMap<SyncList> sDataLists
中取出缓存,- 第一次来这里无法取出值,进入第四步
- 第二次在进来(从不同的线程/过了10秒后在新的地方加锁)用相同的object加锁,此时这里可以取到上次使用过的SyncData *直接使用,并进行
threadCount++
的操作
- 4.新建
SyncData *
赋值给result,并将其保存到哈希表StripedMap<SyncList> sDataLists
中 - 5.如果在当前线程的
tls
中没有缓存过SyncData *
,则将其缓存在当前线程的tls
中,并将lockCount
初始化为1
也缓存在tls
中 - 6.如果
tls
不可用,则将SyncData *
和初始化为1
的lockCount
缓存在cache中
至此整个@synchronized
加锁时的底层源码就分析清楚了,由于底层实现中保存了加锁线程数量-threadCount
和当前线程加锁次数-lockCount
两个参数,所以才能实现多线程重入的递归锁
。
解锁的过程和加锁类似,这里就不看源码实现了,感兴趣的小伙伴可以自己看下objc_sync_exit
的源码实现。