@synchronized详解
如果你已经使用 Objective-C 编写过任何并发程序,那么想必是见过 @synchronized
这货了。@synchronized
结构所做的事情跟锁(NSLock, 更准确的说法应该是递归锁NSRecursiveLock)类似:它可以防止不同的线程同时执行同一段代码。但在某些情况下,相比于使用 NSLock
创建锁对象、加锁和解锁来说,@synchronized
用着更方便,可读性更高, 自然效率会比较低。
递归锁: 同一个线程可以重复的加锁而不会导致死锁(互斥锁: 重复加锁会导致死锁) ,加的递归锁全部执行完后 才会把资源让给别的线程。不同的线程要求加锁会陷入等待.
下面是SDWebImage中的代码, 给一个线程不安全的NSMapTable加递归锁, 保证NSMapTable的线程安全.
在上面的例子中, @synchronized
与 [_lock lock]
和 [_lock unlock]
效果相同。你可以把它当成是锁住 self
,仿佛 self
就是个 NSLock
。锁在左括号 {
后面的任何代码运行之前被获取到,在右括号 }
后面的任何代码运行之前被释放掉。再也不用担心我忘记调用 unlock
了!
你可以给任何 Objective-C 对象上加个 @synchronized
。效果和 @synchronized(self)
是相同的。
回到研究上来
我对 @synchronized
的实现十分好奇并搜了一些它的细节。我找到了一些答案,但这些解释都没有达到我想要的深度。锁是如何与你传入 @synchronized
的对象关联上的?@synchronized
会保持(retain,增加引用计数)被锁住的对象么?假如你传入 @synchronized
的对象在 @synchronized
的 block 里面被释放或者被赋值为 nil
将会怎么样?这些全都是我想回答的问题。而我这次的收获,会要你好看????。
@synchronized
的文档告诉我们 @synchronized
block 在被保护的代码上暗中添加了一个异常处理。为的是同步某对象时如若抛出异常,锁会被释放掉。
SO 上的这篇帖子 说 @synchronized
block 会变成 objc_sync_enter
和 objc_sync_exit
的成对儿调用。我们不知道这些函数是干啥的,但基于这些事实我们可以认为编译器将这样的代码:
@synchronized(obj) {
// do work
}
转化成这样的东东:
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
objc_sync_enter
和 objc_sync_exit
是什么鬼?它们是如何实现的?在 Xcode 中按住 Command 键单击它们,然后进到了 <objc/objc-sync.h>
,里面有我们感兴趣的这两个函数:
/**
* 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);
不过,objc_sync_enter
的文档告诉我们一些新东西: @synchronized
结构在工作时为传入的对象分配了一个递归锁。分配工作何时发生,如何发生呢?它怎样处理 nil
?幸运的是 Objective-C runtime 是开源的,所以我们可以马上阅读源码并找到答案!
你可以在这里找到 objc-sync
的全部源码。在代码块的下方我将立刻做出解释,所以尝试理解代码时别花太长时间哦。
typedef struct SyncData {
id object;
recursive_mutex_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData;
typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList;
// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
一开始,我们有一个 struct SyncData
的定义。这个结构体包含一个 object
(嗯就是我们给 @synchronized
传入的那个对象)和一个有关联的 recursive_mutex_t(底层实现的递归锁)
,它就是那个跟 object
关联在一起的锁。每个 SyncData
也包含一个指向另一个 SyncData
对象的指针,叫做 nextData
,所以你可以把每个 SyncData
结构体看做是链表中的一个元素。最后,每个 SyncData
包含一个 threadCount
,threadCount
就是递归锁在同一线程的加锁次数。每次成功的获得该锁都必须平衡调用锁住和解锁的操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。当threadCount==0
就表明了这个 SyncData
实例可以被其他线程获得了。
下面是 struct SyncList
的定义。正如我在上面提过,你可以把 SyncData
当做是链表中的节点。每个 SyncList
结构体都有个指向 SyncData
节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。
上面代码块的最后一行是 sDataLists
的声明 - 一个 SyncList
结构体数组,大小为16。通过定义的一个哈希算法将传入对象映射到数组上的一个下标。值得注意的是这个哈希算法设计的很巧妙,是将对象指针在内存的地址转化为无符号整型并右移五位,再跟 0xF
做按位与运算,这样结果不会超出数组大小。 LOCK_FOR_OBJ(obj)
和 LIST_FOR_OBJ(obj)
这俩宏就更好理解了,先是哈希出对象的数组下标,然后取出数组对应元素的 lock
或 data
。一切都是这么顺理成章哈。
当你调用 objc_sync_enter(obj)
时,它用 obj
内存地址的哈希值查找合适的 SyncData
,然后将其上锁。当你调用 objc_sync_exit(obj)
时,它查找合适的 SyncData
并将其解锁。
噢耶!现在我们知道了 @synchronized
如何将一个锁和你正在同步的对象关联起来,我希望聊聊当一个对象在 @synchronized
block 当中被释放或设为 nil
时会发生什么。
如果你看了源码,你会注意到 objc_sync_enter
里面没有 retain
和 release
。所以它没有保持传递给它的对象。我们可以用下面的代码来做个测试:
NSDate *test = [NSDate date];
// This should always be `1`
NSLog(@"%@", @([test retainCount]));
@synchronized (test) {
// This will be `2` if `@synchronized` somehow
// retains `test`
NSLog(@"%@", @([test retainCount]));
}
两次输出结果都是 1
。那么 objc_sync_enter
貌似是没保持被传入的对象啊。这就有趣了。如果你正在同步的对象被释放了,然后有可能另一个新的对象在此处(被释放对象的内存地址)被分配内存。有可能某个其他的线程试着去同步那个新的对象(就是那个在被释放的旧对象的内存地址上刚刚新创建的对象)。在这种情况下,另一个线程将会阻塞,直到当前线程结束它的同步 block。这看起来并不是很糟。这听起来像是这种事情实现者早就知道并予以接受。我没有遇到过任何好的替代方案。
假如对象在 “synchronized block” 中被设成 nil
呢?我们再回顾下实现吧:
NSString *test = @"test";
@try {
// Allocates a lock for test and locks it
objc_sync_enter(test);
test = nil;
} @finally {
// Passed `nil`, so the lock allocated in `objc_sync_enter`
// above is never unlocked or deallocated
objc_sync_exit(test);
}
objc_sync_enter
被调用时传入的是 test
而 objc_sync_exit
被调用时传入的是 nil
。而传入 nil
的时候 objc_sync_exit
是个空操作,所以将不会有人释放锁。这真操蛋!
如果 Objective-C
容易受这种情况的影响,我们知道么?下面的代码调用 @synchronized
并在 @synchronized
block 中将一个指针设为 nil
。然后在后台线程对指向同一个对象的指针调用 @synchronized
。如果在 @synchronized
block 中设置一个对象为 nil
会让锁死锁,那么在第二个 @synchronized
中的代码将永远不会执行。我们将不会在控制台中看见任何东西打印出来。
NSNumber *number = @(1);
NSNumber *thisPtrWillGoToNil = number;
@synchronized (thisPtrWillGoToNil) {
/**
* Here we set the thing that we're synchronizing on to `nil`. If
* implemented naively, the object would be passed to `objc_sync_enter`
* and `nil` would be passed to `objc_sync_exit`, causing a lock to
* never be released.
*/
thisPtrWillGoToNil = nil;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^ {
NSCAssert(![NSThread isMainThread], @"Must be run on background thread");
/**
* If, as mentioned in the comment above, the synchronized lock is never
* released, then we expect to wait forever below as we try to acquire
* the lock associated with `number`.
*
* This doesn't happen, so we conclude that `@synchronized` must deal
* with this correctly.
*/
@synchronized (number) {
NSLog(@"This line does indeed get printed to stdout");
}
});
当我们执行上面的代码时,那行代码确实打印到控制台了!所以 Objective-C 很好地处理了这种情形。我打赌是编译器做了类似下面的事情来解决这事儿的。
NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
objc_sync_enter(synchronizeTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizeTarget);
}
用这种方式实现的话,传递给 objc_sync_enter
和 objc_sync_exit
总是相同的对象。他们在传入 nil
时都是空操作。这带来了个棘手的 debug 场景:如果你向 @synchronized
传递 nil
,那么你就不会得到任何锁而且你的代码将不会是线程安全的!如果你想知道为什么你正收到出乎意料的竞态(race),确保你没向你的 @synchronized
传入 nil
。你可以在 objc_sync_nil
上设置一个符号断点来达到此目的。objc_sync_nil
是一个空方法,当 objc_sync_enter
函数被传入 nil
时会被调用,这让 debug 更容易些。
下面是 objc_sync_enter/
objc_sync_exit 的源码,主要逻辑很容易看懂, 加了些注释,加深理解:
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
// 开始在obj上执行同步操作, 懒加载生成一个递归锁关联obj, 返回OBJC_SYNC_SUCCESS
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 查找这个obj是否已经生成SyncData,如果没有生成一个
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock(); // 调用SyncData的递归锁加锁
} else {
// @synchronized(nil) does nothing
// 如果传入nil, 打印了一个log,然后什么都不做
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
// 结束在obj上的同步操作,
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
//还是找到这个对象所在的结构体SyncData
SyncData* data = id2data(obj, RELEASE);
if (!data) {
// 如果这个结构体在block执行过程中找不到了,会返回error
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
// 尝试解锁,解锁失败也会返回error
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
// 如果这个对象在block执行过程中变成nil了,会什么都不做
}
return result;
}
最后回答上述的问题:
锁是如何与你传入 @synchronized
的对象关联上的?
- 你调用
sychronized
的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。
@synchronized
会保持(retain,增加引用计数)被锁住的对象么?
- 使用
@synchronized不会导致此对象的引用计数增加
假如传入 @synchronized
的对象在 @synchronized
的 block 里面被释放或者被赋值为 nil
将会怎么样?
- 如果在
sychronized
内部对象被释放或被设为nil
看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。
如果传入@synchronized
的对象值为 nil
将会怎么样?
-
@synchronized(nil)不会有任何作用,hash计算为空,加锁失败,
代码块不是线程安全的。你可以通过在objc_sync_nil
上加断点来查看是否发生了这样的事情。
最后总结一下@synchronized的原理, @synchronized使用传入的object的内存地址作key,通过hash map对应的一个系统维护的递归锁。所以不管是传入什么类型的object,只要是有内存地址,就能启动同步代码块的效果。如果传入nil, 那就相当于没有加锁.
上一篇: 理解Native Crash处理流程
下一篇: OC之RunLoop了解一下
推荐阅读
-
@synchronized详解
-
StringBuffer&StringBuilder详解 博客分类: java基础
-
StringBuffer&StringBuilder详解 博客分类: java基础
-
Linux-搭建http服务器及配置文件详解,认证用户
-
struts.xml详解 博客分类: Struts2 strutsweb
-
struts.properties配置详解 博客分类: Struts2
-
struts.properties配置详解 博客分类: Struts2
-
centos7下 Mysql+Keepalived 双主热备高可用图文配置详解
-
TCP-IP协议详解(2) 以太网与WiFi协议
-
Java Servlet Cookie 详解