本文首发于 我的博客git
在IOS开发中,同步锁相信你们都使用过,即 @synchronized
,这篇文章向你们介绍一些 @synchronized
的原理和使用。github
@synchronized
是IOS多线程同步中性能最差的:数组
倒是使用起来最方便的一个,一般咱们这么用:sass
@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
显示汇编,打上断点启动真机,就看到了以下代码:markdown
上图中的全部方法的调用我都圈出来了,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;
}
复制代码
从官方代码和注释中咱们能够得出:async
@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
,可能就会出现线程安全问题,这块我会在后面去验证。oop
咱们的@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
5static StripedMap<SyncList> sDataLists;
6
7// SyncList结构体,内部data就是SyncData
8struct SyncList {
9 SyncData *data;
10 spinlock_t lock;
11 constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
12};
13
14// 哈希表结构
15class 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
中传入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
讲清楚,欢迎指正和沟通。