这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
以上内容的总结专栏
细枝末节整理
前言
上一篇,我们分析了锁的基本使用,接下来我们逐一将iOS开发中常用的锁做一个探索,今天我们就先来看看 @synchronized 这把互斥锁的流程吧。
@synchronized 流程分析
准备
线程局部存储 (Thread Loacl Storage, TLS)
是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下 通常通过 pthread 库中的
- pthread_key_create()
- pthread_getspecific()
- pthread_setspecific()
- pthread_key_delete()
posix_memalign 支持内存对齐
编译定位源码位置
想要知道其底层结构,那么,常规操作 xcrun 一下, 在.cpp文件中得到以下内容:
- main函数中的定义
@synchronized (appDelegateClassName) { }
- 编译后的代码:
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
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;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
简单分析一下,我们发现重点内容如下:
try{}代码块中的内容。objc_sync_enter(_sync_obj);objc_sync_exit(sync_exit);
全局搜索没有 没有找到以上内容的具体实现内容,接下来,我们通过符号断点来看下 @synchronized 是在哪一个库中实现:
可以看到其具体实现是放在 libobjc.A.dylib 中。
下面我们通过 libobjc 源码 来分析下其内部实现的流程。
流程探索
objc_sync_enter
// 在'obj'上开始同步
// 如果需要,分配与'obj'关联的递归互斥锁
// 一旦获得锁,返回OBJC_SYNC_SUCCESS
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// obj 存在
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
data->mutex.lock();
} else {
// obj 不存在 什么事情也不做
// @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)
);
...
// 什么也没有实现
/* 对于打算作为断点钩子的函数使用此方法
如果不这样做,编译器可能会对它们进行优化
BREAKPOINT_FUNCTION( void stop_on_error(void) ); */
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
...
objc_sync_exit
// 在'obj'上结束同步
// 返回 OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
//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 {
//obj 不存在 什么事情也不做
// @synchronized(nil) does nothing
}
return result;
}
可以看到 在 objc_sync_enter 和 objc_sync_exit 中 对于 被锁对象obj 为空的时候都是什么事情也不会做。
在obj存在的情况下, 两者也分别调用了 id2data 这个函数, 不过 前者传参数 ACQUIRE, 后者则是 RELEASE, 接下来的操作不同的是,前者是 进行了 data->mutex.lock(); 加锁操作, 后者 则是 data 存在的时候 data->mutex.tryUnlock(); 解锁的操作。 一个加锁,一个解锁 这也正是锁的常规操作。
这里我们那还应该关注下 data 的结构, 也就是 SyncData,因为 是这个数据中封装的锁做的加锁解锁的操作。
下面,我们先解读下 SyncData 的数据结构,然后再重点看下 id2data 这个函数的内部实现来 继续 探索 @synchronized 这把互斥锁的底层实现流程。
SyncData 数据结构
SyncData 是一个结构体:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // 使用此块的线程数
recursive_mutex_t mutex; //递归锁
} SyncData;
-
当看到第一个
nextData指针的时候,我猜想 这个 SyncData 是一个单向链表结构(因为 SyncData 的结构会通过 nextData 指向下一个SyncData数据 )。 -
threadCount 记录使用线程的数,也就是说
支持多线程。 那么,具体是如何实现的多线程也可以使用 @synchronized 呢? 我们接下来通过流程分析来看一下。
id2data 流程分析
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// 全局静态变量 哈希表
static StripedMap<SyncList> sDataLists;
// 哈希函数 确定一个下标
// 可能会发生冲突 (之前通过在哈希来解决)
// 这里 使用 拉链法
...
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
//支持线程局部存储
// 检查每个线程的单条目快速缓存是否匹配对象
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
// 第一部分
if (data) { ... }
#endif
// 检查已经拥有的锁的每个线程缓存是否匹配对象
SyncCache *cache = fetch_cache(NO);
// 第二部分
if (cache) { ... }
//线程缓存没有找到任何东西
//遍历正在使用的列表以查找匹配的对象
//自旋锁防止多个线程创建多个
//锁定相同的新对象
//我们可以把节点保存在某个哈希表中,如果有的话
//有20多个不同的锁在工作中,但我们现在不这么做。
//这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦, 此处是一个 spinlock_t 在上面有定义
lockp->lock();
// 第三部分
// 代码块
{ ... }
// 分配一个新的SyncData并添加到列表中.
// XXX在持有全局锁的情况下分配内存是不好的做法,
// 可能值得释放锁,重新分配,再搜索一次.
// 但由于我们从不释放这些人我们不会经常陷入分配中.
// 第四部分
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(); //这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦,此处是一个 spinlock_t 在上面有定义
// 第五部分
if (result) { ... }
return result;
}
第一部分 和 第二部分
同一个线程会来到这里处理。
如果支持线程局部存储 则 通过线程局部存储来返回 SyncData, 如果不支持, 则 通过线程缓存 来存取 SyncData;
从 TLS 中 获取到 data(SyncData), 然后 判断下 why
处理下ACQUIRE和RELEASE 然后将data return 出去。
第一部分
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) {
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;
}
}
和第一部分流程相似,不过是从cache中取出来,然后进行操作。
第二部分
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--;
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;
}
}
第三部分
不同的线程会在这里处理。
当我们 锁的对象 第一次进来 id2data 流程的时候, 第三部分并不会做什么事情。
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// 原子的,因为可能会与并发的RELEASE冲突
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// 没有当前与对象关联的SyncData
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// 找到一个没用过的,就用它
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
第四部分
result 是当前 从 TLS 或者 缓存中 通过 被锁对象object 拿到的 SyncData。 将其 nextData 指向 了自己;这里涉及到一个 头插法。 也就是每次新来的数据会插在之前数据的前边。
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;
第五部分
对之前查询到的数据 进行 TLS 或者 线程缓存的存储
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++;
}
}
LLDB 调试分析
我们自定义一个使用 @synchronized 的例子来调试一下:
SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [SMPerson alloc];
SMPerson *p3 = [SMPerson alloc];
SMPerson *p4 = [SMPerson alloc];
SMPerson *p5 = [SMPerson alloc];
SMPerson *p6 = [SMPerson alloc];
dispatch_async(dispatch_queue_create("superman", DISPATCH_QUEUE_CONCURRENT), ^{
NSLog(@"0");
@synchronized (p1) {
NSLog(@"1");
@synchronized (p2) {
NSLog(@"2");
@synchronized (p1) {
NSLog(@"1-1");
@synchronized (p1) {
NSLog(@"1-1-1");
@synchronized (p3) {
NSLog(@"3");
@synchronized (p4) {
NSLog(@"4");
@synchronized (p5) {
NSLog(@"5");
@synchronized (p6) {
NSLog(@"6");
}
}
}
}
}
}
}
}
});
第一次进来时 obj = p1; TLS 和 缓存 中 均为空。
会来到 第四部分(初始化一个新的SyncData并添加到列表中) 和 第五部分(对 tls 和 缓存 做存储) 内部实际做了 tls 的存储 处理。
第二次进来时 obj = p2;
在 TLS 或 缓存中 拿到了上一次加锁的 SyncData,
流程和上面的一样,不过在第五部分是走的 线程缓存,没走 tls 的存储
第三次进来 obj = p1;
会来到 第一部分 执行,判断时上一次的 对象一样的话向下执行, 进入 ACQUIRE 分支, 对 lockCount++; 对 将锁的次数通过 SYNC_COUNT_DIRECT_KEY 在 tls 存储, 并返回 tls 存储中取出来的 SyncData。 下次进来判断时同一个对象的话,也会从 SYNC_COUNT_DIRECT_KEY 取出来 锁的次数。
第四次进来 obj = p1;
执行顺序和上一次的一样。
总结
- @synchronized 这把锁在 使用中, 会在维护一张全局的哈希表(
staticStripedMap sDataLists;), 使用拉链法来存储 SyncData; - sDataLists, 中array 存储的是 SyncList (绑定的是objc,我们加锁的对象);
- objc_sync_enter 和 objc_sync_exit 调用对称,封装的是递归锁;
- 两种存储方式 TLS 和 线程缓存;
- 第一次 SyncData采用头插法 在链表结构中, 标记 threadCount = 1;
- 判断是否同一个对象:如果是,lockCount++; 否: 重复上一步。
- lockCount--,lockCount == 0 : threadCount--;
用一张图来总结以上流程:
@synchronized 为什么 具备可重入、 可递归、 可多线程?
因为: TLS 存储了 threadCount 标记 线程数; lockCount 标记了加锁次数; 对锁对象的加锁。
关于 SyncList 结构
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// 全局静态变量 哈希表
static StripedMap<SyncList> sDataLists;
// 哈希函数 确定一个下标
// 可能会发生冲突 (之前通过在哈希来解决)
// 这里 使用 拉链法
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // 使用此块的线程数
recursive_mutex_t mutex; //递归锁
} SyncData;
在 id2data 流程分析 中,有关于 sDataLists 的底层结构,在不同的架构环境中容量稍有不同, SyncList 存储在其中, SyncList中存储 了 SyncData,而SyncData是一个链表的结构。由此,形成了一下的拉链结构:
关于 TLS 和 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++;
}
这一部分的细节: 如果是同一个线程在操作 @synchronized 锁的话,不能持续的对 TLS 进行 无限制串处理(会对TLS造成很大的压力),所以,只有在 切换线程的时候才会去 TLS, 其他情况 都会来到 thread cache (内存缓存)来存储。