本篇提纲
1、锁的简介
2、锁的性能分析
3、synchronized实现分析
4、synchronized中的SyncData结构
5、StripedMap的数据结构
6、synchronized的执行流程
1.锁的简介
我们在使用多线程的时候,可能会遇到多个线程同时访问同一个数据,导致数据错乱和数据不安全的问题,所以就需要使用线程同步。而最常见的线程同步的方式就是加锁
,以保证同一时间只有同一个线程在访问共享数据。
2.锁的性能分析
我们通过代码十万次循环,在循环中进行加锁,解锁的方式,来看一下各种锁对循环的时间影响。下面分别是真机和,模拟器运行的结果。
真机是iPhone11 iOS 15,模拟器是iPhone11 iOS 15
通过运行结果可以看到@synchronized
这种锁,在真机和模拟器上的表现差别很大,真机上性能要比模拟器好一些。而@synchronized
也是我们最常用的锁,这篇文章主要就来研究下@synchronized
的数据结构和内部的具体实现。
3.synchronized实现分析
我们通过符号断点的方式或者clang编译一下,跟踪到@synchronized
对应的代码是这两句:objc_sync_enter
,objc_sync_exit
,我们在源码中看一下这两个方法的具体实现。
- objc_sync_enter
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;
}
首先如果传入的参数
obj
不存在,那么会走objc_sync_nil 方法
,进一步看,这个方法是一个宏定义,然后是空实现。也就是说,如果是obj
为空,就什么都不做。如果
obj
存在,那么会走上边的if
分支,这里边包括了一个新的结构体SyncData
,我们后边会详细看下它的结构。objc_sync_exit
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;
}
id2data
方法在objc_sync_enter
和objc_sync_exit
中都有调用,而且这两个方法中的代码实现也非常的相似,都是去判断obj
,为空就什么都不做,有值就去走id2data
方法,我们来具体看下这个方法的实现。点进去发现大概有一百六十行左右,还挺多的。
static SyncData* id2data(id object, enum usage why)
{
//1、传入object,从哈希表中获取数据
//mutex_tt->os_unfair_lock 根据里面的文档翻译 是自旋锁
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
//传入object,从哈希表中获得SyncData的地址。
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;
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;
//2、在当前线程中的tls中寻找
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: {
//锁+1
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
//再存储到tls中
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
//锁的个数减完之后为0了
if (lockCount == 0) {
// remove from fast cache
//删除局部存储
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
//对SyncData对象的threadCount进行-1,因为当前线程中的对象已经解锁了
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
//3、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;
// Found a match. 匹配到了
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
//这个部分的执行和在TLS中的类似
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();
{
SyncData* p;
SyncData* firstUnused = NULL;
//4、遍历syncList,如果无法遍历,证明当前object的list不存在,需要创建。
for (p = *listp; p != NULL; p = p->nextData) {
//查到了对象
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
//对threadCount+1
OSAtomicIncrement32Barrier(&result->threadCount);
//跳转至done
goto done;
}
//没查到 记录下object的位置
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;
}
}
// 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对象 并且添加到syncList中
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
{
// 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;
}
SUPPORT_DIRECT_THREAD_KEYS:支持线程占存,线程占存
TLS
。TLS
,线程局部存储(Thread Local Storage,TLS),是操作系统为线程单独提供的私有空间,通常只有有限的容量。ACQUIRE
在方法objc_sync_enter
传入的值,对lockCount进行+1操作,并存储。RELEASE
在方法objc_sync_exit
传入的值,对lockCount进行-1操作,并进一步判断lockCount的值是不是为0,如果为0,对threadCount进行-1操作。done
对list中找到的object而在TLS或者cache没有找到的对象,进行TLS存储,或者cache存储,并且进行一些错误判断。-
链表头插法
4.synchronized中的SyncData结构
SyncData
:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
- SyncData中又有一个
struct SyncData* nextData;
相同类型的指向下一个节点的一个next,所以这是一个单向链表
,节点中存储了下一个节点的地址。 - threadCount使用block块的线程数
- recursive_mutex_t递归锁,底层还是os_unfair_lock。
5.StripedMap的数据结构
我们通过代码看到SyncData
是从LIST_FOR_OBJ
中取出来的,
SyncData **listp = &LIST_FOR_OBJ(object);
进一步看LIST_FOR_OBJ
它的定义是
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
是一个宏,而sDataLists
是一个静态表
static StripedMap sDataLists;
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
StripedMap
是哈希类型,所以sDataLists
是一张静态哈希表,内部存储SyncData
,而SyncData
本身又是单链表,所以StripedMap
是哈希表+单链表的结构。
而StripedMap
解决哈希冲突的方法是通过拉链法
,就是如果计算的下标已经存储了内容,那么会存储到SyncData`的next中,如果next还有内容,会继续往下找,直到找到可以存储的位置。
StripedMap
结构示意图
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
};
在真机分配了8个空间,模拟器分配64个。当把模拟器修改成1后,不同的对象来到id2data
时,通过打印可以看到,当冲突了会存到冲突位置的nextData
中。
6.Synchronized的执行流程
通过上面的讨论,可以整理出以下流程。
1、调用@ synchronized(object){}时,相当于调用了方法objc_sync_enter
和objc_sync_exit
。
2、在objc_sync_enter
方法中和objc_sync_exit
方法中首先都是进行对传入的object
判断,如果为nil
就什么都不做;
如果存在,那么objc_sync_enter
和objc_sync_exit
都会调用方法id2data
,只不过方法objc_sync_enter
中传的参数是ACQUIRE
,而objc_sync_exit
传的是RELEASE
,这正好对应了id2data
方法中switch
分支的处理。
3、id2data
中的逻辑是这样:
3.1 首先判断是否支持TLS,如果支持从TLS中查找相关的object存储信息,查到了,入到
switch(why)
的分支判断,如果是ACQUIRE
,那么锁lockCount+1,然后更新存储,返回result
;
如果是RELEASE
,那么锁lockCount-1,然后更新存储,再进一步判断lockCount是不是0,如果为0,threadCount-1操作,然后更新存储。3.2 如果从TLS中没查到,那么查
SyncCache
缓存,进行缓存的遍历,如果查到了这个对象的缓存,进入到switch(why)
的分支判断,如果是ACQUIRE
,那么锁lockCount+1,然后更新存储,返回result
;
如果是RELEASE
,那么锁lockCount-1,然后更新存储,再进一步判断lockCount是不是0,如果为0,threadCount-1操作,然后更新存储。3.3 如果缓存也没查到,那么去遍历object所在的listp中查找,如果查到了,进行threadCount的处理,并且跳转到
done
。done
的操作是,先进行上面查找读取的解锁,然后进行简单的错误判断。如果支持TLS,那么把信息更新到TLS中进行存储(这样下次再来的时候,第一步就可以查到了),如果不支持,那么更新到cache中,下次进来的时候第二步就可以查到了。然后返回result
。3.4 如果list中也没查到,那么创建一个新的SyncData对象 并使用头插法插入到链表中(这样下次再来到list就可以查到返回了,然后执行list往缓存存储的流程),并且返回
result
。