在 iOS 开发常见的几种锁 介绍了常见的几种锁的使用场景以及使用方法,它的底层是如何实现的呢?下面我们带着疑问一起去探索下 @synchronized
的底层原理吧
@synchronized
开发中,在多个线程访问同一块资源的时候,我们会添加以下代码来避免引发数据错乱和数据安全的问题
@synchronized (self) {
//添加执行的代码
}
那我们如何去探索它的底层实现呢?首先我们需要它在底层会调用什么方法,其次我们要知道方法所在的源码库,这样我们才能清晰的知道它的底层是如何实现的。
底层方法调用探究
通过开启汇编调试,我们看到如下 @synchronized
在执行过程中,会走底层的objc_sync_enter
和 objc_sync_exit
方法
此时我们对 objc_sync_enter
方法下一个符号断点,发现底层实现所在的源码库是 libobjc.A.dylib
objc_sync_enter & objc_sync_exit
打开 objc-781
源码工程,查看 objc_sync_enter
的源码实现如下:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 执行 ACQUIRE 操作,返回 data 数据
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;
}
再查看 objc_sync_exit
的源码,实现如下:
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 执行 RELEASE 操作,返回 data 数据
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;
}
从源码中我们可以看到,如果传入的 obj
为 nil
,则什么都不做;如果传入的 obj
不为 nil
,则会获取相应的 SyncData
对它进行一系列的操作。那么这个 SyncData
是什么?它的结构是什么样的?SyncData
的定义如下:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
从 SyncData
的定义可以看到,它是一个结构体。第一个成员变量指向下一个 SyncData
,是一个链表结构;第四个成员属性代表递归属性。从 SyncData
结构就可以看出 @synchronized
是一个递归互斥锁。
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
这里顺便查看 SyncCache
的结构,后续会调用到。SyncCache
是一个结构体对象,用于存储线程。其中 list[0]
表示当前线程的链表 data
,主要用于存储 SyncData
和 lockCount
id2data 源码分析
这里源码很长,先总体看下流程,后面详细分析
整体分为四大步
- 快速缓存
如果支持快速缓存,就从快速缓存中读取线程和任务,再进行相应操作
- 线程缓存
快速缓存没找到,就从线程缓存中读取线程和任务,再进行相应操作
上述代码中 fetch_cache
函数进行缓存查询和开辟
- 循环遍历
所有的缓存都找不到,循环遍历每个线程和任务,再进行相应操作
- Done
如果有错误,则抛出异常;如果正常,则存入快速缓存(前提是支持快速缓存)和线程缓存中,便于下次快速查找
每个被锁的
object
对象可拥有一个或多个线程
拓展
以下代码,运行后会发生什么?
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
我们运行项目,看看会发生什么
项目运行就崩溃了,原因在与 self.testArray
会触发 set
方法,而 set
方法本质是新值 retain
,旧值 release
,而在异步调用时,可能会造成多次调用 release
(上一次的 release
还没结束,下一次的 release
已经来了),导致野指针,从而 crash
。
- 验证
我们打开Xcode 工程 -> Edit Scheme... ->Run -> diagnostics -> 勾选 Zombie Objects,再次运行项目
调用 [__NSArrayM release]
时,是发送给了 deallocated
(已析构释放)的对象。
僵尸对象是一种用来检测内存错误(EXC_BAD_ACCESS)的,给僵尸对象发送消息时,那么将在运行期间崩溃和输出错误日志。通过日志可以定位到野指针对象调用的方法和类名。
- 添加 @synchronized
既然我们知道了原因,那我们使用 @synchronized
加锁试一下吧(这是 @synchronized 错误示范😄)
NSLog(@"123");
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
再次运行项目,还是 crash
了,报错信息和上面的一致,这是为什么呢?在上面的源码分析中可以看到,因为锁的对象是 self.testArray
,它会 release
,它释放了,等于锁也就没用了。
正确的使用方法
NSLog(@"123");
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
self.testArray = [NSMutableArray array];
}
});
}
@synchronized
锁的对象,需要确保锁内代码的生命周期。所以将锁对象改为self。就解决问题了。当然也可以用其他锁来解决问题
网友评论