1、八大锁效率
- 八大锁分别:
- 自璇所:OSSpinLock。在
iOS10以后该锁被重写,会在堵塞时进行休眠
; - 互斥锁:NSLock、NScondition、NSRecursiceLock、NSConditionLock、@synchronize;以及更加偏底层:pthread_mutex、pthread_mutex(recursive);
- 自璇所:OSSpinLock。在
2、synchronize探索入口
所有底层的探索都需要一个切入点,像这样的代码段除了堆栈
的方式,还有clang
、查看汇编
的方式。
@synchronized (self) {
i += 1;
}
2.1 查看堆栈
事实证明在这个问题上是不适用的;
2.2 汇编方式
- 可以看到使用了
@synchronize
之后在方法块前后调用了两个方法objc_sync_enter
、objc_sync_exit
;
继续增加objc_sync_enter
的符号断点之后;
-
@synchronize
是属于libobjc.A.dylib
库的; -
objc_sync_enter
在底层callq(调用)函数id2data(objc_object*, usage)
;
2.3 clang方式
使用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o vc.cpp
- 根据
clang
获取编译后的代码,也可以看到熟悉的两个方法objc_sync_enter
、objc_sync_exit
,同时也验证了汇编方式的结论;
3、objc_sync_enter 源码分析
通过符号断点,得知@synchronize
是在我们熟悉的libobjc
库中,在我之前的文章中可以得到OC底层探索02- objc4-781 源码编译;
enum usage { ACQUIRE, RELEASE, CHECK };
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// ACQUIRE 枚举值
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;
}
BREAKPOINT_FUNCTION(
//其实什么都没有做
void objc_sync_nil(void)
);
看到这段代码之后再回头看看
objc_sync_enter
的汇编部分,是不是发现其实汇编也就那样;
-
data->mutex.lock()这才是真正的加锁操作
,是系统recursive_mutex_t
递归互斥锁的更高层封装; - 如果传入的
obj是个空值
,系统是没有做任何事的,所以在使用时要保证标示对象一定不能为空
; - 通过异常判断之后进入函数
id2data(obj, ACQUIRE)
;
4、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(obj, RELEASE)
只是第二个参数不一样。提现了无处不在的抽象和封装思想;
5、id2data(obj, enum usage
) 核心函数
代码非常长,这里分为四步分来分析
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
// 包含当前对象的链表
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) {
// 第一部分
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
// 第二部分
}
lockp->lock();
//第三部分
done:
lockp->unlock();
if (result) {
// 第四部分
}
return result;
}
3.3.1 第一部分 快速缓存
#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;
// 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
// 缓存次数为0后,将快速缓存对象制空
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
// 原子性的对缓存对象的线程使用数减一
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
}
//找到处理完lockCount后直接返回
return result;
}
}
#endif
- 在没有特别设置:
SUPPORT_DIRECT_THREAD_KEYS默认为1;
- 当前缓存的快速缓存: 当前线程
第一次加锁的对象会被定义为快速缓存
;(大多数情况下,一条线程只会使用一个标示对象进行加锁); -
SYNC_DATA_DIRECT_KEY
、SYNC_COUNT_DIRECT_KEY
都是在当前线程的局部缓存
中查找缓存对象SyncData
、缓存次数lockCount
;
3.3.1 SyncData
在快速缓存阶段,系统保存了结构为SyncData
的对象。
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr object;
int32_t threadCount; // 使用该锁的线程数
recursive_mutex_t mutex; // 递归互斥锁
} SyncData;
-
SyncData
锁对象对象,是一个链表结构; -
SyncData
将synchronize锁所需要的数据进行保存;
3.3.2 第二部分 慢速缓存
这一部分涉及到了慢速缓存
,如果在快速缓存中没有找到则会来到这部分;
// 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;
// 这部分和快速缓存操作基本一致
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) {
// 缓存数组的总个数减少
cache->list[i] = cache->list[--cache->used];
// 原子性操作
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
}
return result;
}
}
- 测试后发现,慢速缓存也是从
当前线程的进行查找
; - cache->list[i] = cache->list[--cache->used];
将数组最后一个对象移动到当前下标位置,然后将数组进行缩容;
- 通过这个双重缓存结构,提高了锁对象
syncdata
的查找效率;
3.3.2 SyncCache
在慢速缓存中出现了这样一个结构SyncCache
.
typedef struct {
SyncData *data; // 锁对象
unsigned int lockCount; // 缓存次数
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used; // 缓存数组的个数
SyncCacheItem list[0]; // 锁对象的列表
} SyncCache;
-
SyncCache
是慢速缓存的实体体现; -
SyncCacheItem
包含了SyncData
锁对象以及该锁对象的缓存次数;
3.3.3 第三部分
在双重缓存下都没有命中后会来到这部分,这部分会在:初次加锁
、同一对象不同线程加锁
的时候进入.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
// listp在函数最开始进行的获取
for (p = *listp; p != NULL; p = p->nextData) {
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;
// an unused one was found, use it
// 找到一个未使用的,使用它,(复用)
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// 全新创建
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;
-
同一对象不同线程加锁时会进行原子的threadCount++
; - 由于list中SyncData不会进行删除,所以
需要复用
; - 如果1、2步都没有名字,则进行
全新创建
,并保存到节点的第一个;
3.3.3 StripedMap
listp
是在函数最开始进行获取,锁对象存储结构。通过对object
的地址hash计算后确定数组下标;
SyncData **listp = &LIST_FOR_OBJ(object);
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap sDataLists;
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
//哈希算法
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
}
- StripedMap在OC底层探索19-weak和assign区别浅谈在分析weak存储结构时也出现过,都是通过hash算法来进行分组,减少数据查找的难度;
- 不同的是
weak的StripedMap
对应的是一张SideTable
表;而@synchronized的StripedMap
对应的是一个链表结构
;
3.3.4 第四部分 done
done:
lockp->unlock();
if (result) {
// 解锁流程
if (why == 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
// 快速缓存未被占用则保存
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
// 否则保存到当前线程的慢速缓存list中
{
// 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;
- 在第三部分处理完之后都会来到done中;
-
快速缓存和慢速缓存会互斥存在
;
总结
通过函数id2data
的参数完成了加、解锁操作。并且使用了快速缓存、慢速缓存双重缓存,来提高synvData的命中速度
。除此之外stiped
+syncData链表
对锁实体进行保存。利用threadCount
+lockCount
实现了多线程、重复加、解锁操作;
通过这些操作提高了递归锁的安全性,但是也降低了性能;
补充
线程局部存储(Thread Local Storage,TLS):是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下通常通过pthread库中的。
还有的几种锁,以后有机会在探索吧~毕竟大部分都在Founation
库中,不是很好分析。
欢迎在留言区和我沟通!