如果你曾经接触过Objective-C中的并发编程的话,那么你肯定见过@synchronized
结构。@synchronized
常用于避免并发过程中同时执行一段相同的代码。相对于分配以及加锁,解锁一个NSLock
对象来说,使用@synchronized
提高了代码的可读性,且易于理解。
如果你从没有用过@synchronized
的话,下面有个简单的例子展示了用法。这篇文章将会简短地介绍@synchronized
的实现方式。
@synchronized使用例子
假定我们使用Objective-C实现一个线程安全的队列,我们可能会:
@implementation ThreadSafeQueue {
NSMutableArray *_elements;
NSLock *_lock;
}
- (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)push:(id)element {
[_lock lock];
[_elements addObject:element];
[_lock unlock];
}
@end
复制代码
ThreadSafeQueue
有一个init
方法,初始化了一个_elements
数组和一个NSLock
这两个变量。除此之外,还有一个执行了加锁,给数组添加对象和解锁操作的push:
方法。会有多个线程在同一时间调用push:
方法,但是将对象添加进数组的这个操作只能在同一时间由一个线程进行操作。用文字描述起来的话,就是
- 线程A调用了
push:
- 线程B调用了
push:
- 线程B调用了
[_lock lock]
- 由于没有其他对象持有该锁,所以线程B需要加锁 - 线程A调用
[_lock lock]
,但是由于线程B正持有lock,所以这个方法并不会返回,而是暂停了线程A的执行 - 线程B将对象添加进数组,并调用
[_lock unlock]
。当这个操作完成的时候,线程A的[_lock lock]
方法有了返回。线程A开始继续执行。
如果使用@synchronized
的话,代码就是:
@implementation ThreadSafeQueue {
NSMutableArray *_elements;
}
- (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
}
return self;
}
- (void)increment {
@synchronized (self) {
[_elements addObject:element];
}
}
@end
复制代码
sychronized的功能跟上一个例子中的[_lock lock]
和[_lock unlock]
是一样的。你可以认为这段代码中利用NSLock
对self
进行加锁。lock的添加必须在{
执行之后,而解锁则需在}
执行之前。这相对来说比较容易,毕竟我们不再需要调用unlock
方法!
你可以将@synchronized
运用到其他Objective-C的对象中,所以我们能够将上文的@synchronized(self)
替换成@synchronized(_elements)
,结果是一毛一样的。
回到研究
我对@synchronized
的实现方式感到好奇,因此在开始在谷歌上搜索相关的资料。我找到一些答案,但是这些答案并没有很深入地分析出我所想要的答案。lock是如何与传递进@synchronized
的对象进行相关联的?在locking期间,@synchronized
会retain该对象吗?这些都是我想知道的,所以接下来我将展示我所能找到的相关的知识点。
@synchronized
的文档 告诉我们@synchronized
块隐式地将执行片段添加到受保护的代码中。所以当同步一个特殊对象时如果抛出异常的话,lock对象也会被释放。
SO的这篇答案则说@synchronized
会在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中cmd+click,我们会进入到`<objc/objc-sync.h>中,我们可以看到:
// Begin synchronizing on 'obj'
// Allocates recursive pthread_mutex associated with 'obj' if needed
int objc_sync_enter(id obj)
// End synchronizing on 'obj'
int objc_sync_exit(id obj)
// The wait/notify functions have never worked correctly and no longer exist.
int objc_sync_wait(id obj, long long milliSecondMaxWait);
int objc_sync_notify(id obj);
复制代码
ok, objc_sync_enter
的文档告诉我们一些知识点:@synchronized
在给某个对象分配一个递归锁并传递时才会工作起来。但是分配是发生在什么时候?又是如何分配的?如何处理nil
值?还好OC的runtime是开源的,我们能够从中获取到答案
在这里可以看到objc-sync
的完整代码,但是我会带你进入一个更容易的理解中。看着文件顶部的数据结构,我将通过下面的例子来解释这些数据,不用花太多的时间来解读他们。
typedef struct SyncData {
id object;
recursive_mutext_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData;
typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList;
static SyncList sDataLists[16];
复制代码
首先,我们看到struct SyncData
的定义。该结构包含了一个object
(这就是我们传递进来的对象),和一个相关联的recursive_mutex_t
(与对象相关联的lock)。每个SyncData
都包含了另一个SyncData
的称为nextData
的指针,所以你可以将每个SyncData
结构看成链表中的元素。最后,每个SyncData
包含了一个threadCount
对象来表明当前SyncData
对象使用或者等待中的线程数目。所以,如果SyncData
被缓存了的话,在threadCount == 0
时,就表明SyncData
实例被重用了。
然后我们看到了struct SyncList
的定义,正如上面所说的,可以将SyncData
看成列表中的一个节点。每个SyncList
结构都有指针指向SyncData
的起始地址,而且lock阻止多线程中的并发修改。
最后一行是SyncList
结构体的数组sDataLists
的声明。可能一眼看起来不太像,但是sDataLists
数组是一个哈希table,将OC对象映射到相对应的lock中。
当你调用objc_sync_enter(obj)
时,它使用obj的内存地址的哈希值来寻找相关联的SyncData
并且进行上锁。当你调用objc_sync_exit
时,则是寻找对应的SyncData
进行解锁。
Great!现在我们知晓了@synchronized
的工作方式。接下来我将告诉你的是在@synchronized
中如果碰到对象被销毁或者变为nil
的情况
如果你阅读了源码的话,你会发现在objc_sync_enter
中并没有retains
或者是releases
。所以它并不会retain我们传递进来的对象,或者是通过arc进行编译。我们可以用以下的代码来测试以下:
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
并不会retain传递给它的对象。有点亦可赛艇~,如果同步过程中,该对象被销毁了,然后在这个地方新分配了一个对象。其他线程就有可能会将这个新的对象当成原来的那个。在这个例子中,其他线程将会被阻塞直到当前线程完成。听起来也没啥的样子。
如果该对象变为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
。所以最后objc_sync_exit
并没有成功执行,也就意味着lock没有被释放
下面的代码中将一个指针传递给了@synchronized
,并且在执行过程中变为nil
。然后在后台线程中调用@synchronized
。如果在第一个@synchronized
中,在解锁之前对象变为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_DEFAULT_PRIORIT_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");
}
});
复制代码
但是, 当我们运行上面的代码时,在控制台中能看到输出!所以我猜测OC之所以能处理这种情况,可能大概执行了如下代码吧:
NSString *test = @"test";
id synchronizedTarget = (id)test;
@try {
objc_sync_enter(syncrhonizedTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizedTarget);
}
复制代码
通过这种方式,objc_sync_enter
和objc_sync_exit
都能获取到同一个对象。但是这会引起一个bug就是当你将nil
传递给@synchronized
时,并没有执行任何的lock,此时状态是非线程安全的。
所以现在能够得出结论就是
- 对于每个调用
synchronized
的对象,OC的Runtime都会分配一个同步锁并且存储进哈希表 - 在
synchronized
执行过程中如果对象被销毁或者置为空是可以的。但是这并没有文件表明这一结论 - 不要将
nil
传递给synchronized
。这将导致你的代码变为非线程安全。