synchronized

@synchronized

@synchronized 是iOS多线程同步机制中最慢的一个,同时也是最方便的一个。
那么 @synchronized 内部到底是如何实现的?如果传入一个空值会发生什么?

@synchronized 原理

首先我们可以 main.m 文件中写下如下测试代码

1
2
3
4
5
6
void testSync() {
NSObject* obj = [NSObject new];
@synchronized (obj) {

}
}

通过 xcode (Product->Perform Action->Assemble “main.m”) 我们可以得到如下汇编代码

我们发现 @synchronized 最终被转换成如下两个函数

1
2
_objc_sync_enter
_objc_sync_exit

我们可以从 objc4 中找到这两个函数的源代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;


// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
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;
}

// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
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;
}

从上述源代码中我们发现,

1
2
synchronized 是使用的递归互斥锁来做同步。
@synchronized(nil)不起任何作用

递归锁是互斥锁的变体。递归锁允许单个线程在释放之前多次获取锁。其他线程保持阻塞状态,直到锁的所有者释放锁的次数与获取锁的次数相同。在递归迭代期间主要使用递归锁,也可以在多个方法分别需要获取锁的情况下使用递归锁。synchronized 递归锁内部使用的还是 pthread_mutex_t,只是加了一层ownership 的判断。

传入的 obj 对象如何被用作参数来获取 SyncData 对象

查看 static SyncData* id2data(id object, enum usage why) 函数我们发现

1
SyncData **listp = &LIST_FOR_OBJ(object);

而 LIST_FOR_OBJ 又指向了

1
2
3
4
// 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
static StripedMap<SyncList> sDataLists;

在找到 StripedMap 我们从内部看到如下代码

1
2
3
4
5
6
7
8
9
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}

indexForPointer 使用了obj的内存地址,并且做了一个简单的map,映射到另一个内存空间来存放SyncList。

通过上述我们可以得出:

synchronized中传入的object的内存地址,被用作key,通过hash map对应的一个系统维护的递归锁。

因此不管我们传入什么类型的obj,只要有内存地址,就能正确的启动代码同步块的效果

慎用 @synchronized(self)

看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}

//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];

self很可能会被外部对象访问,被用作key来生成一锁,类似代码中的@synchronized (objectA)。这时两个公共锁交替使用就容易出现死锁。所以正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的。

精准的粒度控制和注意内部函数调用

@synchronized 和其他锁对比性能上要差一点,这一点在对开发者来说可忽略不计,除了本身的原因,更多的是开发者本身不正确的使用造成的。因此正确的使用很重要。我们需要注意以下两点:

1.对于不同的临界区应该使用不同的锁

2.注意内部隐藏代码的调用

1
2
3
4
5
6
7
8
@synchronized (tokenA) {
[arrA addObject:obj];
}

@synchronized (tokenB) {
[arrB addObject:obj];
[self doSomethingWithA:arrA];
}

doSomethingWithA内部可能又调用了其他函数,由此层层叠叠可能引入更多的函数调用,代码就莫名其妙的越来越慢了,感觉锁的性能差。

以上内容参考以下文章

关于 @synchronized,这儿比你想知道的还要多

正确使用多线程同步锁@synchronized()