在我们的日常开发中肯定都有过锁的使用,那么这些锁的底层原理是如何实现的呢?各种锁的性能区别又有多大呢?在这一篇章我们来探究一下。
各种锁的性能分析
int cx_runTimes = 100000;
/** OSSpinLock 性能 */
{
OSSpinLock cx_spinlock = OS_SPINLOCK_INIT;
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
OSSpinLockLock(&cx_spinlock); //解锁
OSSpinLockUnlock(&cx_spinlock);
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"OSSpinLock: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** dispatch_semaphore_t 性能 */
{
dispatch_semaphore_t cx_sem = dispatch_semaphore_create(1);
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
dispatch_semaphore_wait(cx_sem, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(cx_sem);
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"dispatch_semaphore_t: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** os_unfair_lock_lock 性能 */
{
os_unfair_lock cx_unfairlock = OS_UNFAIR_LOCK_INIT;
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
os_unfair_lock_lock(&cx_unfairlock);
os_unfair_lock_unlock(&cx_unfairlock);
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"os_unfair_lock_lock: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** pthread_mutex_t 性能 */
{
pthread_mutex_t cx_metext = PTHREAD_MUTEX_INITIALIZER;
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
pthread_mutex_lock(&cx_metext);
pthread_mutex_unlock(&cx_metext);
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"pthread_mutex_t: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** NSlock 性能 */
{
NSLock *cx_lock = [NSLock new];
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
[cx_lock lock];
[cx_lock unlock];
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"NSlock: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** NSCondition 性能 */
{
NSCondition *cx_condition = [NSCondition new];
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
[cx_condition lock];
[cx_condition unlock];
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"NSCondition: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** PTHREAD_MUTEX_RECURSIVE 性能 */
{
pthread_mutex_t cx_metext_recurive;
pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&cx_metext_recurive, &attr);
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
pthread_mutex_lock(&cx_metext_recurive);
pthread_mutex_unlock(&cx_metext_recurive);
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** NSRecursiveLock 性能 */
{
NSRecursiveLock *cx_recursiveLock = [NSRecursiveLock new];
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
[cx_recursiveLock lock];
[cx_recursiveLock unlock];
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"NSRecursiveLock: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** NSConditionLock 性能 */
{
NSConditionLock *cx_conditionLock = [NSConditionLock new];
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
[cx_conditionLock lock];
[cx_conditionLock unlock];
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"NSConditionLock: %f ms",(cx_endTime - cx_beginTime)*1000);
}
/** @synchronized 性能 */
{
double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < cx_runTimes; i++) {
@synchronized(self) {}
}
double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
CXLog(@"@synchronized: %f ms",(cx_endTime - cx_beginTime)*1000);
}
在这里我们通过代码对 10 种锁进行了测试,并制作了表格,这里是在 iphone12 真机环境下进行的,这里我们可以发现一个问题,在我们的印象中 @synchronized
是比较消耗性能的,但是这里的测试的好像还好。这是因为开发过程中 @synchronized
的使用频率比较高,苹果在 arm64
下对 @synchronized
做了性能优化,这里后面我们会进行分析。这 10 种锁里面因为 dispatch_semaphore_t
在讲 GCD
的时候已经分析过了,这里就不在讲了。pthread_mutex_t
跟 pthread_mutex_t(recurive)
因为调用的是 pthread
的 api
,这里也不再讲了。其实我们每种锁的最底层都是基于 pthread
实现的,如果想验证某种锁的性能,跟 pthread
来做比较就好。
@synchronized 分析
@synchronized 原理分析上
因为我们平时开发过程中 @synchronized
使用频率最高,这里我们就来先探索一下 @synchronized
的原理。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
@synchronized (appDelegateClassName) {
}
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
类似这段代码,我们通过生成 cpp
文件来看一下 @synchronized
的底层代码实现。
通过底层代码我们可以看到,如果加锁成功我们需要看的就是 objc_sync_enter(_sync_obj)
跟 objc_sync_exit(_sync_obj)
这两段代码。
我们运行下符号断点,可以看到是在 libobjc.A.dylib
库调的 objc_sync_enter
函数,所以我们下载 libobjc.A.dylib
源码具体来分析一下。
objc_sync_enter
跟 objc_sync_exit
源码探究
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;
}
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
函数刚开始都会先判断 obj
,如果 obj
为空,通过注释也可以看到,相当于什么都不做,然后通过 id2data
函数获取 SyncData
,只是 objc_sync_enter
跟 objc_sync_exit
函数传的参数不一样,且 objc_sync_enter
函数会调用 data->mutex.lock()
加锁, objc_sync_exit
函数会调用 data->mutex.tryLock()
解锁。
-
SyncData
数据结构
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // 类似链表结构,下一个节点
DisguisedPtr object; // 对 object 包装成 DisguisedPtr 结构
int32_t threadCount; // 代表线程数量
recursive_mutex_t mutex; // 通过 pthread 定义了一个递归锁 mutex
} SyncData;
id2data
函数分析
通过上面对 objc_sync_enter
跟 objc_sync_exit
函数的分析,可以看到他们都调用了 id2data
函数,这里我们来重点分析下 id2data
函数。
因为这个函数内的代码比较多,我们先整体分析下这个函数大致做了哪些事情。
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
这里我们来先看看这个函数最开始的时候通过 &LOCK_FOR_OBJ(object)
获取到 lockp
,通过 & LIST_FOR_OBJ(object)
获取到 listp
,这里我们看看这两个宏定义。
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap sDataLists;
这里可以看到,这两个宏定义其实都是对 sDataLists
方法的定义。这里我们也可以看到 sDataLists
是一个全局的哈希表,表里面存储的是 SyncList
结构类型的数据。
SyncList
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
sDataLists
这里我们通过 lldb
来查看一下 sDataLists
的数据结构。
CXPerson *p1 = [[CXPerson alloc] init];
CXPerson *p2 = [[CXPerson alloc] init];
CXPerson *p3 = [[CXPerson alloc] init];
dispatch_async(dispatch_queue_create("cx", DISPATCH_QUEUE_CONCURRENT), ^{
@synchronized (p1) {
@synchronized (p2) {
@synchronized (p3) {
}
}
}
});
通过打印我们可以看到 StripedMap
里面存储的每个元素是 SyncList
,SyncList
的 data
是 SyncData
数据结构的链表。
这个 StripedMap
是一张全局的哈希表,每个象对应一个 SyncList
,同一个对象每加锁一次会对 data
链表插入一个 SyncData
,虽然都是一个对象,但是 SyncData
不同,当对对象解锁的时候就会删除对应的 SyncData
。
id2data
函数执行流程
这里我们详细的来分析一下 id2data
函数的执行流程。
-
id2data
函数第一次执行
-
id2data
函数第二次执行 (@synchronized
参数不是同一个对象)
-
@synchronized
加锁同一个对象,且不是第一次
这里 OSAtomicDecrement32Barrier
函数会对 threadCount
减 1,threadCount
代表同一个对象在不同线程进行加锁,线程的数量。
-
@synchronized
加锁同一个对象,且不是第一次并且不在同一个线程
@synchronized
总结
- 1:
@synchronized
会有一张全局哈希表sDataLists
,数据存储采用的是拉链法 - 2:
sDataLists
是一个array
,存储的是SyncList
,SyncList
跟objc
对应。 - 3:
objc_sync_enter
函数跟objc_sync_ exit
函数成对出现,底层是基于pthread
封装的递归锁 - 4: 支持两种存储 : tls / cache
- 5: 第一次调用
id2data
函数,会创建一个syncData
并进行头插法,生成一个链表,并标记thracount = 1
。 - 6: 判断是不是同一个对象进来
- 7: TLS -> lockCount ++
- 8: TLS 找不到上一个
SyncData
,会重新创建一个SyncData
,并对threadCount ++
。 - 9:
lockCouture--
,threadCount--
。
@synchronized
支持递归并支持多线程的原因:
TLS
保障了可以用threadCount
来标记有多少条线程对这个锁对象进行加锁。
lockCount
用来标记在当前线程空间锁对象被加锁了多少次。
补充
-
TLS
线程相关解释
线程局部存储
(Thread Local Storage,TLS)
: 是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux
系统下通常通过pthread
库中的pthread_key_create()
、pthread_getspecific()
、pthread_setspecific()
、pthread_key_delete()
。
-
@synchronized
使用注意事项
@synchronized
参数不要为空。- 要注意
@synchronized
加锁的对象的生命周期
@synchronized
加锁对象为同一个对象时方便数据的存储与释放(这里有一个问题就是会导致SyncList
链表过长,会对内存操作行成负担,但是一般不会出现这种情况)。@synchronized
真机比模拟器性能高的原因
通过源码可以看到真机 StripeCount
为 8,模拟器 StripeCount
为 64。StripeCount
越大数据存储的就会越大,数据操作的时候需要查询的数据也会越多,这是导致真机比模拟器性能高的原因。