本文首发于 个人博客
在IOS开发中,同步锁相信大家都使用过,即 @synchronized
,这篇文章向大家介绍一些 @synchronized
的原理和使用。
@synchronized 原理
@synchronized
是IOS多线程同步中性能最差的:
却是使用起来最方便的一个,通常我们这么用:
@synchronized (self) {
// code
}
为了了解其底层是如何 实现的,我们在测试工程的ViewController.m中写了一段代码
static NSString *token = @"synchronized-token";
- (void)viewDidLoad {
[super viewDidLoad];
@synchronized (token) {
// code
NSLog(@"haha");
}
}
点击Xcode
--> Debug
-->Debug Workflow
--> Always Show Disassembly
显示汇编,打上断点启动真机,就看到了如下代码:
上图中的所有方法的调用我都圈出来了,ARC
帮我们自动插入了retain
和release
,而且还找到了NSLog的方法,那么包含NSLog的正是 objc_sync_enter
和 objc_sync_exit
我们猜测这个应该就是 @synchronized
的具体实现,所以我们来到 Objective-c源码 处查找,很快我们发现了这两个函数的实现:
// 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;
}
从官方代码和注释中我们可以得出:
- 使用
@synchronized
会创建一个递归(recursive)
互斥(mutex)
的锁与obj
参数进行关联。 -
@synchronized(nil)
does nothing
递归锁其实就是为了方便同步锁的嵌套使用而不会出现死锁的情况:
static NSString *token = @"synchronized-token";
static NSString *token1 = @"synchronized-token1";
- (void)viewDidLoad {
[super viewDidLoad];
@synchronized (token) {
NSLog(@"haha");
@synchronized (token1) {
NSLog(@"didi");
}
}
}
@synchronized(nil) 不起任何作用,说明我们要注意传入obj
的生命周期,因为当obj被释放这个地方就起不到加锁的作用,有同学可能注意到我这为什么没有用self 作为obj传递参数,就是为了避免token
被多个地方持有修改,一旦出现nil
,可能就会出现线程安全问题,这块我会在后面去验证。
obj 是如何保存的
我们的@synchronized
是针对传入参数obj
做绑定的,那么内部obj
究竟是干嘛用的,而且我们知道@synchronized
在object-c
全局都可以使用,那么@synchronized
是如何区分不同的obj进行一一对应的,带着这些问题,我们看看底层对obj究竟是如何处理的。
首先我们看到底层是这样处理传入的对象 obj 的:
SyncData* data = id2data(obj, ACQUIRE);
内部都会将 obj
转化成相应的 SyncData
类型的对象,然后 id2data
内部是下面这样取的:
SyncData **listp = &LIST_FOR_OBJ(object);
看看LIST_FOR_OBJ
是如何操作obj
的(下面代码留下关键部分,具体细节请自行前往源码查看):
1 // obj传入sDataLists
2 #define LIST_FOR_OBJ(obj) sDataLists[obj].data
3
4 // 哈希表结构,内部存SyncList
5 static StripedMap<SyncList> sDataLists;
6
7 // SyncList结构体,内部data就是SyncData
8 struct SyncList {
9 SyncData *data;
10 spinlock_t lock;
11 constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
12 };
13
14 // 哈希表结构
15 class StripedMap {
16 enum { StripeCount = 64 };
17
18 struct PaddedT {
19 T value alignas(CacheLineSize);
20 };
21
22 PaddedT array[StripeCount];
23
24 // 哈希函数
25 static unsigned int indexForPointer(const void *p) {
26 uintptr_t addr = reinterpret_cast<uintptr_t>(p);
27 return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
28
29 }
30
31 public:
32 // 此处的p就是上面的obj,也就是obj执行上面的哈希函数对应到数组的index
33 T& operator[] (const void *p) {
34 return array[indexForPointer(p)].value;
35 }
从上述代码看出整体StripedMap
是一个哈希表结构,表外层是一个数组,数组里的每个位置存储一个类似链表的结构(SyncList
),SyncData
存储的位置具体依赖第25
行处的哈希函数,如图:
obj1
处,经过哈希函数计算得出索引2
,起初我们要顺着上面的 A
线对List
进行查找,没找到,将当前的obj
插入到最前面,也是为了更快的找到当前使用的对象而这么设计。
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
obj2
处就不多分析了,找到直接返回,进行加锁解锁处理。
慎用@synchronized(self)
@synchronized
中传入object
的内存地址,被用作key
,通过一定的hash
函数映射到一个系统全局维护的递归锁中,所以不论传入什么类型的值,只要它有内存地址就可以达到同步锁的效果。
引用这篇文章开头所说的例子:
通常我们直接用@synchronized(self)
没毛病,但是很粗糙,也确实存在问题。是不是他喵的被我说乱了,到底有没有问题?
@property (nonatomic, strong) NSMutableArray *array;
for (NSInteger i = 0; i < 20000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self->_array = [NSMutableArray array];
});
}
这块代码我们知道是有问题的,会直接Crash
,原因就在于多线程同时操作array,导致在某一个瞬间可能同时释放了多次,也就是野指针的问题。那么我们尝试用今天的 @synchronized
来同步锁一下:
for (NSInteger i = 0; i < 20000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self->_array) {
self->_array = [NSMutableArray array];
}
});
}
调试一下发现依然崩溃,@synchronized
不是加锁吗,怎么还会Crash
呢?
原来我们绑定的对象是array,而内部多线程对array的操作却是频繁的创建和release,当某个瞬间arry执行了release的时候就达成了我们所说的 @synchronized(nil)
,上文已经分析了这个时候do nothing !
所以能起到同步锁的作用么,很显然不能,这就是崩溃的主要原因。
由此可见@synchronized
在一些不断的循环,递归的时候并不如人意,我们唯独要注意obj的参数唯一化,也就是与所要锁的对象一一对应,这样就避免了多个地方持有obj。
static NSString *token = @"synchronized-token";
@synchronized (token) {
self.array1 = [NSMutableArray array];
}
--------------------------------------------------
static NSString *another_token = @"another_token";
@synchronized (another_token) {
self.array2 = [NSMutableArray array];
}
总结
虽然 @synchronized
使用起来很简单,但是其内部的实现却是很复杂,由于系统维护了一个全局的哈希表,而我们的@synchronized
又不停的对这个哈希表进行增删改查,所以其性能很差,但是适当的地方使用也无可厚非,无非就是注意对象的生命周期,以及内部的嵌套等。希望这篇文章能把@synchronized
讲清楚,欢迎指正和沟通。