以下内容仅是学习synchronized过程中的一些笔记, 没有进行思维逻辑的整理,所以读起来跳跃性可能较大,不过不妨碍理解 synchronized的一些原理性知识点,这里可以带着以下几个问题阅读源码:
- 锁是如何与传入
@synchronized
的对象关联上的 - 是否会对关联的对象有强引用
- 如果synchronize传入nil会有什么问题
- 假如传入
@synchronized
的对象在@synchronized
的 block 里面被释放或者被赋值为nil
将会怎么样 synchronized
是否是可重入的,即是否为递归锁 寻找底层实现入口一般有clang编译和汇编断点两种方式, 这里使用clang进行编译
通过clang 编译出的cpp显示,@synchronized(self){int a = 10;}
最终转换成了以下代码:
{
// 对象
id _rethrow = 0;
id _sync_obj = (id)self;
// 同步
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, 当调用析构函数时执行objc_sync_exit
_sync_exit(_sync_obj);
// 执行任务
int a = 10;
}
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);
}
}
相比其他同步锁, 做了异常捕获相关的嵌入代码, 在没有成对处理锁时,不会直接死锁crash,而是抛出异常
注意到的两个核心方法就是 objc_sync_enter和objc_sync_exit
/**
* Begin synchronizing on 'obj'.
* Allocates recursive pthread_mutex associated with 'obj' if needed.
*
* @param obj The object to begin synchronizing on.
*
* @return OBJC_SYNC_SUCCESS once lock is acquired.
*/
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
/**
* End synchronizing on 'obj'.
*
* @param obj The object to end synchronizing on.
*
* @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
*/
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
enum {
OBJC_SYNC_SUCCESS = 0,
OBJC_SYNC_NOT_OWNING_THREAD_ERROR = -1
};
注释中需要注意的是objc_sync_enter是通过pthread_mutex 递归锁实现的
两个函数源码可以再objc/objc-sync中找到
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 使用关联对象创建节点
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 节点内上锁
data->mutex.lock();
}
return result;
}
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
// ...
}
return result;
}
无论是enter还是exit,其实核心代码都是id2data(),通过传入不同的枚举值表示不同的逻辑分支
id2data主要处理的内容是使用SyncData链表结构采用拉链法解决哈希冲突, 在缓存处理上支持了TLS/SyncCache 两种方式,
主要逻辑如下
- 查找当前线程的tls快速缓存, TLS由于是线程私有,所以直接使用对应的key存储lockCount和data, 这里需要注意fastCacheOccupied的判断条件, 只要存在一个SyncData就会为yes, 所以tls存储的一直都是链表的首节点,后续的SyncData都是存储到SyncCache中
- 查找syncCache缓存,
fetch_cache
的底层仍然是通过tls存储的, 所以该缓存同样也是线程隔离的, 同时又依赖于SyncCacheItem针对每一个线程存储对应的lockCount和data, 和第一步目的是一样的, 保证不同关联对象之间数据的隔离 - 查找全局字典syncDataLists, 该字典是一个全局字典, 通过将对象地址做偏移映射到对应index的syncData
- 如果仍未找到, 则创建新的syncData, 先判断是否存在未使用的syncData, 存在直接使用,不存在创建对应数据结构,采用头插法插入到syncDataLists 当前对象拉链中的头结点位置
- 将新的syncData缓存到tls/ syncCache中, 下次使用
具体逻辑并不是很复杂, 可以直接看代码中的注释
static SyncData* id2data(id object, enum usage why)
{
// 通过object 获取dataList中的 data和 lock
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;
// 通过tls存储对应的key, tls是线程私有的小容量存储空间, 是一个字典
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
// =====================================存在快速缓存 Syncronized的数据结构, 在快速缓存中查找
fastCacheOccupied = YES;
// 递归的话, 则只处理计数即可, SyncData本身是递归锁
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;
}
}
#endif
// ==================快速缓存没匹配, 在SyncCache缓存中查找
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
// 遍历cache中所有在使用的对象. 匹配到本次加锁的对象
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
// 和OSAtomicIncrement32Barrier同理, 防止共享资源冲突
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;
// 遍历字典sDataList中 匹配本次加锁对象的数据结构, 每一个对象存储的是一个链表
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
// 这里使用原子操作, 防止多线程增加threadCount时和RELEASE操作冲突, 因为加锁只是当前逻辑部分加锁, 可能另一个线程正在进行上面的缓存中同一个关联对象的RELEASE操作
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// 存在SyncData 节点的关联线程已经不存在, 在作为头节点使用, 性能优化
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
// 没有和当前对象关联的SyncData 对象, 如果是release 或者check则直接执行done, 其他例如ACQUIRE创建, 则新建result对象/或者使用旧的数据结构
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
// 使用旧的数据结构存储新的数据,不再需要创建result, 直接进行done操作
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.
// 创建新的result操作, 这里采用头插发
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;
}
几个数据结构成员变量的作用可以参考以下注释
typedef struct alignas(CacheLineSize) SyncData {
// 下一个节点, SyncData可以理解成链表结构
struct SyncData* nextData;
// synchronize关联的对象
DisguisedPtr<objc_object> object;
// 使用这个block的线程数, 因为 SyncData 结构体会被缓存,threadCount==0 就暗示了这个 SyncData 实例可以被复用。
int32_t threadCount; // number of THREADS using this block
// 和对象关联的递归锁, 执行代码块时真正生效的锁, 所以synchronize是一个可递归的锁
recursive_mutex_t mutex;
} SyncData;
// 用于缓存相关
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block 这个锁锁定该block的次数
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
/*
Fast cache: two fixed pthread keys store a single SyncCacheItem.
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
*/
// 正如上面提过,你可以把 SyncData 当做是链表中的节点。每个 SyncList 结构体都有个指向 SyncData 节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。
struct SyncList {
// 节点
SyncData *data;
// 这个锁是为了解决多线程中生成syncdata对象时冲突的问题
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.
// 使用多个列表减少不相关对象的竞争
// 通过宏直接获取列表中关联对象的SyncList的data和lock数据
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
syncDataLists是StripedMap泛型字典, 该字典在iOS系统中默认是8个元素, Mac OS中是64个
一句话总结就是: synchronized
通过对objec的封装, 关联了对应的lock结构, 通过lock实现同步锁,
回到开头的四个问题
-
锁是如何与你传入
@synchronized
的对象关联上的通过对象地址关联的, 即任何存在内存地址的对象都可以作为synchronize的key使用
-
是否会对关联的对象有强引用
没有强引用, 只是将内存地址作为key使用2022.3.14 更正:
通过 clang 查看编译后的内容, 存在
id _sync_obj = (id)self;
语句,如果是 ARC 下这里会造成 retain 操作,通过汇编也可以看到调用了 objc_retain, 后续的objc_sync_enter(_sync_obj);
内是没有强引用操作的。所以严格来说ARC 下是会有强引用的。MRC 环境下没有强引用。 -
如果synchronize传入nil会有什么问题
通过entry源码发现, 传入nil 会调用
objc_sync_nil
, 而BREAKPOINT_FUNCTION 对该函数的定义为asm()"")
即空汇编指令, 不会做任何事情 -
假如你传入
@synchronized
的对象在@synchronized
的 block 里面被释放或者被赋值为nil
将会怎么样被释放后, 不会执行任何代码, 此时的锁也没有被释放,即一直处于锁定状态
-
synchronized
是否是可重入的,即是否为递归锁 是可递归的, 因为内部是对os_unfair_recursive_lock的封装, os_unfair_recursive_lock结构通过os_unfair_lock和count实现了可递归的功能
使用注意事项
慎用@synchronized(self)
为了避免该对象在外部也被作为synchronized的key使用, 造成死锁的状况, 所以尽量不使用self作为key, 应该使用外部不可见的私有变量作为key, 以下示例会造成synchronized和_sharedLock的死锁
//class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];
精准的粒度控制
通过源码可以看到, synchronized相比其他锁只是多了查找过程, 性能效率不会过低, 之所以慢是更多的因为没有做好粒度控制
例如以下示例
@synchronized (sharedToken) {
[arrA addObject:obj];
}
@synchronized (sharedToken) {
[arrB addObject:obj];
}
使用同一个token来同步arrA和arrB的访问,虽然arrA和arrB之间没有任何联系。传入self的就更不对了。
应该是不同的数据使用不同的锁,尽量将粒度控制在最细的程度。上述代码应该是:
@synchronized (tokenA) {
[arrA addObject:obj];
}
@synchronized (tokenB) {
[arrB addObject:obj];
}
另外因为锁执行时会锁定当前代码块, 所以应该避免内部调用函数, 其他开发者可能并不知道函数链上的函数处于锁定状态