在利用 objc 进行多线程编程时常常遇到同步的问题,这时用的最多的就是NSLock
和@synchronized
,@synchronized
较NSLock
使用起来会方便很多、可读性较高。
本文以一个例子开头,请问下述代码的输出结果是什么:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [NSObject new];
dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);
__block int a = 0;
for (int i = 0; i < 10; i++) {
dispatch_async(queue1, ^{
@synchronized (obj) {
NSLog(@"queue1, a = %d", a++);
obj = nil;
}
// obj = [NSObject new];
});
}
for (int i = 0; i < 10; i++) {
dispatch_async(queue2, ^{
@synchronized (nil) {
NSLog(@"queue2, a = %d", a++);
}
});
}
for (int i = 0; i < 10; i++) {
dispatch_async(queue3, ^{
@synchronized ([NSObject new]) {
NSLog(@"queue3, a = %d", a++);
}
});
}
// 延迟主线程退出,主线程退出子线程也会退出
sleep(2);
}
return 0;
}
上述输出结果是:三个循环中使用的锁均无效(包括第一个循环中注释无论是否打开)。下面就@synchronized
的实现原理进行剖析。
NSObject *obj = [NSObject new];
@synchronized (obj) {
NSLog(@"hello world.");
}
利用clang -rewrite-objc xxx
将上述代码转化如下,代码中已经对关键内容进行了注释。这里说几个点:
- 代码中用到了很多的代码块(
{}
结构),是为了在执行到}
时,代码块中的对象释放,触发析构函数的调用; - 锁入口函数
objc_sync_enter
,退出函数objc_sync_exit
; - 如果锁的释放出现了异常,则会由
catch
块捕获,最终在FIN
中抛出;
// 创建 obj 对象
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
{
id _rethrow = 0;
id _sync_obj = (id)obj;
// 锁的入口函数
objc_sync_enter(_sync_obj);
try {
// 创建一个 _SYNC_EXIT 类型的对象,该对象在 try 代码块执行完成后会调用其析构函数释放,最终执行 objc_sync_exit 释放锁。
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
// NSLog(@"hello world.");
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h5_359yk7215js43kb5v40w49sc0000gn_T_test_65e99a_mi_0);
} catch (id e) {
// 捕获异常
_rethrow = e;
}
{
// 如果捕获了异常,则会触发 _FIN 初始化的时候其 rethrow 变量有值,并在对象释放是调用析构函数抛出异常。
struct _FIN {
_FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);
}
}
objc_sync_enter
和objc_sync_exit
究竟做了什么?我们查看一下相关源码,戳这里。
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;
}
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;
}
从中我们可以看出,当obj
为空时,什么也没做。所以@synchronized(nil){}
并不能达到锁的效果。我们发现上述两个函数中都通过id2data
获取结构体SyncData
。
如下代码,我列出了几个关键结构:
// 链表结点
typedef struct SyncData {
struct SyncData* nextData; // next,说明是个链表结构
DisguisedPtr object; // synchronized 中的 obj 最终传递到这里
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex; // 互斥锁
} SyncData;
// 链表
struct SyncList {
SyncData *data;
spinlock_t lock; // 访问该链表的锁
SyncList() : data(nil) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// 链表存储的位置,StripedMap 用于存储 void* -> T,即地址映射,具体内容可以查看 StripedMap
static StripedMap sDataLists;
// 缓存时链表中的 node 结构
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
对于synchronized
传入的obj,有两条链表进行存储,一条链表的node
结构是SyncData
,用于正常访问用;另一条是SyncCacheItem
,从名字中可以看出是做缓存用,SyncCacheItem
中有个SyncData
类型的属性。
SyncData
是从函数id2data
中获取的,该函数内容较多,因为我们重点是关注synchronized
传入的对象是干什么用的,所以我简单解释下该函数的内容:
- 查看缓存中是否有,判断标准是缓存中
SyncCacheItem.data.object
与synchronized
传入的obj
的地址是否相等; - 如果缓存中没有,则在
sDataLists
中查找,判断标准也是对象地址; - 创建
SyncData
,并存储在sDataLists
中; - 存储到缓存中;
id2data
中第 3 步会创建SyncData
对象,从中可以看到synchronized
中传入的obj
最终存储在SyncData->object
中。
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object; // obj
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
result->nextData = *listp;
// 添加到 sDataLists 链表中
*listp = result;
总结:从@synchronized(){}
的执行流程我们可以得出如下结论:
- 不要传递
nil
对象,因为nil
导致block
执行时没有使用锁; - 两次执行
synchronized
传入不同的对象,同步操作失效; - 传递的
obj
对象,起作用的主要是对象地址,对象地址与使用的锁一一对应; - 如果在
@synchronized(){}
的block
中将obj
置为nil
,从代码分析synchronized
退出后,锁并不会被释放。那造成的结果是什么呢?要么下次访问synchronized
传入的是新对象,要么下次传入的是上次的obj
(此时为nil)。这两种情况对应上述结论1、2,都会导致同步执行失效。