这是我参与8月更文挑战的第20天,活动详情查看:[8月更文挑战]
当我们提到线程时,就会联想到线程不安全,如何确保线程安全以及多线程之间数据访问如何保证不出问题呢?带着这些疑问我们来介绍一下锁的原理。
1 iOS中的锁
1.1 锁的分类
锁分为两大类自旋锁、互斥锁,其中自旋锁是通过忙等(不停询问)实现的,适用于给短小的任务加锁。
- 自旋锁
- OSSpinLock
- dispatch_semaphore_t
- os_unfair_lock_lock
- 互斥锁
- pthread_mutex_t
- NSLock
- NSCondition
- NSRecursiveLock
- NSConditionLock
- @synchronized
1.2 各种锁的性能
测试各种锁加锁/解锁10万次的表现
int kRunTimes = 100000;
{
某锁 lock = 初始化锁;
double_t beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < kRunTimes; i++) {
加锁(&lock);
解锁(&lock);
}
double_t endTime = CFAbsoluteTimeGetCurrent() ;
NSLog(@"某锁: %f ms",(endTime - beginTime)*1000);
}
复制代码
真机iPhoneXR运行结果
OSSpinLock: 1.433015 ms
dispatch_semaphore_t: 2.267957 ms
os_unfair_lock_lock: 2.338052 ms
pthread_mutex_t: 2.584100 ms
NSlock: 2.802968 ms
NSCondition: 2.210975 ms
PTHREAD_MUTEX_RECURSIVE: 2.773046 ms
NSRecursiveLock: 3.018975 ms
NSConditionLock: 5.580902 ms
@synchronized: 9.202957 ms
复制代码
真机iPhone12mini运行结果
OSSpinLock: 0.748038 ms
dispatch_semaphore_t: 1.023054 ms
os_unfair_lock_lock: 0.805020 ms
pthread_mutex_t: 0.934958 ms
NSlock: 1.582980 ms
NSCondition: 1.513004 ms
PTHREAD_MUTEX_RECURSIVE: 2.305984 ms
NSRecursiveLock: 2.532005 ms
NSConditionLock: 8.258939 ms
@synchronized: 3.880978 ms
复制代码
各个锁的运行时间大致可以如下图所示,经过测试,在不同的环境表现不一样,真机表现较好,模拟器表现较差,这说明@synchronized
这个锁在真机上有一定的优化。@synchronized
锁在我们的应用中使用频率相对较高。
2 @synchronized的分析
2.1 @synchronized初探
因为@synchronized
使用最简单最频繁,所以我们首先研究@synchronized
。
- 通过clang转C++代码,查看器底层实现
测试用例
static int a = 0;
int main(int argc, char * argv[]) {
NSObject *obj = [NSObject alloc];
@synchronized (obj) {
a++;
}
return 0;
}
复制代码
通过C++代码,我们可以看到几点:
-
- 加锁解锁针对同一对象_sync_obj,通常我们使用self,因为self直接可以用,避免创建额外锁对象,同时在调用对象方法过程中,不会释放,保证锁对象可用。
-
- 加锁通过
objc_sync_enter(_sync_obj)
,解锁通过objc_sync_exit(_sync_obj)
;@synchronized
的锁代码块,放在加锁、解锁之间,起到加锁作用。
- 加锁通过
static int a = 0;
int main(int argc, char * argv[]) {
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
{
id _rethrow = 0;
id _sync_obj = (id)obj;
objc_sync_enter(_sync_obj);
try {
// 包装exit对象
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
// 加锁代码
a++;
} // exit对象析构, ~_SYNC_EXIT() -> objc_sync_exit(_sync_obj)
catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
return 0;
}
复制代码
- Xcode查看汇编
我们来探究一下objc_sync_enter
,objc_sync_exit
内部实现
2.2 @synchronized源码分析
- objc_sync_enter
// 创建`递归锁`关联`obj`
// 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;
}
复制代码
BREAKPOINT_FUNCTION
相当于给一个函数指针添加空实现, BREAKPOINT_FUNCTION( void objc_sync_nil(void) );
-> void objc_sync_nil(void) {}
.
BREAKPOINT_FUNCTION( void objc_sync_nil(void) );
/* Use this for functions that are intended to be breakpoint hooks.
If you do not, the compiler may optimize them away.
BREAKPOINT_FUNCTION( void stop_on_error(void) ); */
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
复制代码
所以说objc_sync_enter(nil)
,@synchronized(nil) does nothing
.
- objc_sync_exit
同样objc_sync_exit(nil)
也是@synchronized(nil) does nothing
什么都不做.
// 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;
}
复制代码
与此同时, 我们发现objc_sync_enter
,objc_sync_exit
惊人相似, 最大的区别是:
objc_sync_enter
->SyncData* data = id2data(obj, ACQUIRE)
,ACQUIRE 获得
, 加锁objc_sync_exit
->SyncData* data = id2data(obj, RELEASE)
,RELEASE 释放
, 解锁- 说明
@synchronized(obj)
核心在id2data()
的实现, 以及SyncData
的数据类型结构.
2.3 @synchronized(object)的核心实现
objc_sync_enter
与objc_sync_exit
都调用了id2data
,那么它肯定是我们的重心研究对象,我们就顺藤摸瓜分析一下。
我们找下它的源码,如下:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// 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;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
复制代码
代码较复杂,我们一块一块的来分析。
这个函数整个大的代码块
/// 1
if (data) {
/// 逻辑
}
///21
if (cache) {
/// 逻辑
}
/// 3
lockp->lock();{
/// 逻辑
}
/// 4
lockp->unlock();
if (result) {
/// 逻辑
}
复制代码
我们接下来,一段一段的来分析。
#if SUPPORT_DIRECT_THREAD_KEYS需要支持TLS,什么是TLS, 我们来解释下。
线程局部存储(Thread Local Storage, TLS)是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下通常通过pthread库中的
pthread_key_create()、
pthread_getspecific()、
pthread_setspecific()、
pthread_key_delete()
if(data) 和if(cache) 有两个地方存储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;
复制代码
这段代码对result赋值操作,继续走,
done:
lockp->unlock();
复制代码
done了之后,又进行了unlock(),这是为什么呢?
这是因为在内部操作的时候,开辟内存空间,内存空间的相关处理,保证线程安全。
上面我们通过分析SyncData,知道它是一个单向链表,为什么呢,为什么会有两次存储?
spinlock_t lockp = &LOCK_FOR_OBJ(object); 是通过object获取了一把锁,#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock*这是这个宏定义。
*SyncData *listp = &LIST_FOR_OBJ(object); 也是通过个获取的。
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
复制代码
这里为什么会有sDataLists?
sDataLists是一个静态全局变量。
StripedMap是哈希map,通过下标,哈希有时候会出现冲突,通常我们再次哈希来解决,如果再冲突,我们再次哈希,这是常规做法,这次我们介绍一下拉链法。
sDataLists是静态全局变量,StripedMap其实是哈希结构,也就是全局的哈希表,SyncList的结构如下:
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
复制代码
我们通过LLDB调试下,如图:
这里就是sDataLists结构,一共64个数据。
第4个数据,说明我们来了一次,这也间接证明这是哈希结构。
synData数据来源于object。
假如我们锁的是self,又来了self锁,又会创建一个synData存储到哈希表中,这个时候关键词都是self,这时候怎么办,这时候会存在哈希冲突的结构,在我们的SyncList不在是一个数据结构,而是通过链表形式存储,因为两两不会冲突,链表结构,不方便查询,但是SyncData是不需要我们查询的,我们只需要加锁,解锁,只需要增删,不需要查询,这就形成了拉链法。
2.4 synchronized的注意事项
- synchronized锁的对象不要出现空
- synchronized锁的对象的生命周期至少要跟线程代码的对象一样或者比它长,所以一般我们使用self
- synchronized锁对象使用self,可以防止多次创建,方便存储和释放。
- synchronized锁对象使用self,如果当前的对象锁的很长,这个链表就会很大,就会对拉链有一定的负担,不过一般不会操作的很频繁。
- synchronized锁,模拟器与真机耗时之间相差比较大是因为,在模拟器是有64个大小的限制
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
复制代码
因为这里的判断,如果是真机就是8,64个要耗费长,查的也长时间长。