在我们的日常开发中肯定都有过锁的使用,那么这些锁的底层原理是如何实现的呢?各种锁的性能区别又有多大呢?在这一篇章我们来探究一下。
各种锁的性能分析
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<objc_object> 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<SyncList> 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 越大数据存储的就会越大,数据操作的时候需要查询的数据也会越多,这是导致真机比模拟器性能高的原因。














网友评论