关于@synchronized, 这里比你想要了解的还多(译)

原文地址rykap.com/objective-c…


如果你曾经接触过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:方法,但是将对象添加进数组的这个操作只能在同一时间由一个线程进行操作。用文字描述起来的话,就是

  1. 线程A调用了push:
  2. 线程B调用了push:
  3. 线程B调用了[_lock lock] - 由于没有其他对象持有该锁,所以线程B需要加锁
  4. 线程A调用[_lock lock],但是由于线程B正持有lock,所以这个方法并不会返回,而是暂停了线程A的执行
  5. 线程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]是一样的。你可以认为这段代码中利用NSLockself进行加锁。lock的添加必须在{执行之后,而解锁则需在}执行之前。这相对来说比较容易,毕竟我们不再需要调用unlock方法!

你可以将@synchronized运用到其他Objective-C的对象中,所以我们能够将上文的@synchronized(self)替换成@synchronized(_elements),结果是一毛一样的。

回到研究

我对@synchronized的实现方式感到好奇,因此在开始在谷歌上搜索相关的资料。我找到一些答案,但是这些答案并没有很深入地分析出我所想要的答案。lock是如何与传递进@synchronized的对象进行相关联的?在locking期间,@synchronized会retain该对象吗?这些都是我想知道的,所以接下来我将展示我所能找到的相关的知识点。

@synchronized文档 告诉我们@synchronized块隐式地将执行片段添加到受保护的代码中。所以当同步一个特殊对象时如果抛出异常的话,lock对象也会被释放。

SO的这篇答案则说@synchronized会在objc_sync_enterobjc_sync_exit中执行。我们并不知道这两句代码的作用,但是对于编译器来说,可能会是这样的:

@synchronized(obj) {
	// do work
}
复制代码

会转换为

@try {
	objc_sync_enter(obj);
	// do work
} @finally {
	objc_sync_exit(obj);
}
复制代码

objc_sync_enterobjc_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_enterobjc_sync_exit都能获取到同一个对象。但是这会引起一个bug就是当你将nil传递给@synchronized时,并没有执行任何的lock,此时状态是非线程安全的。

所以现在能够得出结论就是

  1. 对于每个调用synchronized的对象,OC的Runtime都会分配一个同步锁并且存储进哈希表
  2. synchronized执行过程中如果对象被销毁或者置为空是可以的。但是这并没有文件表明这一结论
  3. 不要将nil传递给synchronized。这将导致你的代码变为非线程安全。

转载于:https://juejin.im/post/5a30e0006fb9a045186aba7b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值