锁的原理

前言

之前我们分析过多线程,知道了线程之间存在资源竞争的问题,为了解决这个问题,系统推荐了各种,保证当前只有一条线程对资源进行修改,从而保证数据的安全。今天外面重点看看系统提供了哪些,以及它们的底层源码,具体做了哪些流程?

锁的性能

在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:

上图可以看到除了OSSpinLock 外,dispatch_semaphorepthread_mutex 性能是最高的。但是OSSpinLock可能存在优先级反转的问题,那什么是优先级反转呢?

首先我们看看正常情况下的线程调度原则👇

系统将线程分为5个不同的优先级: backgroundutilitydefaultuser-initiateduser-interactive高优先级线程始终会在低优先级线程执行,一个线程不会受到比它更低优先级线程干扰

但是可能存在这种现象👇(优先级反转)

如果一个低优先级的线程获得并访问共享资源,这时一个高优先级的线程也尝试获得这个,它会处于 spin lock忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock,这就会破坏了spin lock,造成优先级反转。

因此,苹果工程师不建议使用OSSpinLock自旋锁,而是尽量使用pthread_mutex替换。

一、@synchronized

@synchronized是我们最为熟悉的互斥锁,我们先来看看其底层实现流程是怎么样的。
示例👇

@synchronized (self) {
    NSLog(@"123");
 }

1.1 寻找入口

打断点,bt查看调用栈信息👇

然并卵,再进入汇编(Always Show Disassembly)👇

发现了两个objc_sync_enter objc_sync_exit,如果还不信,那么直接clang生成cpp验证👇

xcrun -sdk iphonesimulator clang -rewrite-objc filename

然后就是找objc_sync_enterobjc_sync_exit是在哪个库?还是去到汇编中👇


注意:连续step into 2次

找到了入口,原来在库libobjc.A.dylib中。

1.2 objc_sync_enter & objc_sync_exit

先看objc_sync_nil()源码👇

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }

就是asm(""),确实没做任何处理!

至此,我们发现,objc_sync_enterobjc_sync_exit都调用了id2data函数,这个是重点!

1.3 id2data

在查看id2data的源码之前,我们先看看几个重要的数据结构:

  • StripedMap
  • SyncList
  • SyncData

为何将SyncData设计成单向链表这种结构?链表的优势在于插入、删除元素比普通的顺序表快,试想一下这个场景,单个线程里可支持加多个锁,多个线程加多个锁,加锁就好比是插入元素,如果采用顺序表,从头部开始查找,找到你想插入的位置再插入,就比较耗时了,所以采用链表的结构。

以上的分析,可以得出下面这张图👇

  • 最左侧就是StripedMap,可以将它理解为一个哈希表,里面的元素是SyncList
  • SyncList是个结构体,其中包含成员SyncData
  • SyncData里又指向下一个SyncData,使得SyncList形成了一个单向链表的结构
  • 最终,StripedMap里的元素就是一个个的单向链表

再回头来看id2data

以上都是寻找object关联的SyncData对象,没找到就new一个,如果找到呢,做了什么流程?那么再看看👇

  1. 线程栈存中找到的处理
  1. 缓存中找到的处理

首先看看fetch_cache流程👇

接着来到_objc_fetch_pthread_data

找到的pthread数据是个结构体👇

里面的SyncCache👇

再来看看SyncCache里找到SyncData后的处理流程👇

1.4 done代码块的处理流程👇

至此,id2data的流程分析完毕,大致就是:

  1. 查找锁对象object对应的SyncData对象,并返回。
  2. 如果线程栈存缓存object对应的pthread_data里的cacheItem缓存,以及全局静态单向链表里都没找到的话,那么就new一个SyncData对象
  3. 线程栈存缓存找到,就同步一下pthread_data里的cacheItem缓存,反之,也同步一下线程栈存缓存,保证两个缓存数据的同步一致

但同时,要考虑场景,以enter为例:

  1. 第一次进来时
  1. 非第一次,同一个线程时
    如果支持线程栈存👇

不支持线程栈存👇

  1. 非第一次,不同线程时

然后去到done代码块,进行缓存处理

最后回头来看@synchronized的底层源码,就清晰多了,找锁对象object对应的SyncData对象,调用该SyncData对象的成员recursive_mutex_t互斥锁完成加锁,并且加锁支持可重入(lockCount++)多个线程嵌套加(threadCount++)`。

然后看解锁,其实与加锁查找流程一模一样的,只是最后处理的是lockCount--threadCount--👇

二、NSLock & NSRecursiveLock

2.1 NSLock

使用方式很简单,lock unlock即可👇

    NSLock *lock = [[NSLock alloc] init];
    [lock lock];
    NSLog(@"123");
    [lock unlock];

接着我们看看底层的实现👇

老规矩,还是找入口。

  1. 打断点,看汇编,尝试step into👇


step into根本进不去,放弃!

  1. lldb 查看bt👇


还是没找到有用的信息,放弃!

  1. 下符号断点lock👇

发现是Foundation,但是是闭源的。依旧放弃!

  1. 最终,还有一个渠道,就是看swift版本的源码,因为是开源的👇

2.2 NSRecursiveLock

我们先看一个嵌套block的案例

- (void)lg_testRecursive{
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
                if (value > 0) {
                    NSLog(@"current value = %d",value);
                    testMethod(value - 1);
                }
            };
            testMethod(10);
        });
    }
}

运行👇

数据会有重复,说明数据不安全!此时必须加锁,保证线程写数据的安全,
尝试用NSLock👇

直接阻塞了,根本没起到任何作用,因为这是一个递归,同时又是异步并发,是个多线程递归的调用,那么就会出现场景:线程之间会出现相互等待的情况。具体来说就是,线程1上锁后读取value值,还没解锁,此时线程2又进来加锁读取value值,线程2的任务作为了线程1的一个子任务,于是线程1的完成依赖线程2执行完成,但是线程2要执行完,必须等线程1解锁。

应对上述的场景,系统提供了一个递归锁NSRecursiveLock,专门应对这种递归情况,使用如下👇

接下来,我们看看递归锁的底层源码(还是swift开源版本)👇

与NSLock源码对比,发现lockunlock方法是一样的,但仔细看初始化,是有些区别的👇

对当前互斥锁,做了一个PTHREAD_MUTEX_RECURSIVE 类型的设置,这是个递归的类型,而NSLock却没有,说明是默认的类型。

但是递归锁的使用,不是很好,在哪里lock,又在哪里unlock,很容易出错。👇

这就是对锁的使用不熟练导致的,加锁解锁对象 --> 重点在于你要执行的任务,在执行任务前加锁,任务执行完成后解锁。

如果不熟练,不如使用@syncronize,一个代码块搞定,根据我们底层的分析,它更试用于多线程的场景,lockCount++保证锁可重入,threadCount++保证多线程递归👇

三、NSCondition & NSConditionLock

3.1 NSCondition条件变量

NSCondition的对象实际上作为一个锁一个线程检查器

  • 主要为了当检测条件时保护数据源,执行条件引发的任务;
  • 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

对于NSCondition条件变量,有一个经典案例:生产消费者模型
声明 + 初始化👇

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 0;
    [self lg_testConditon];
}

生产者👇

- (void)lg_producer{
     [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];
}

消费者

- (void)lg_consumer{
    // 线程安全
     [_testCondition lock];
    
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
         [_testCondition wait];
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}

调用👇

- (void)lg_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        
    }
}

run👇

问题来了,感觉使用条件变量有些麻烦,除了lock unlock之外,还要一会signal,一会wait,根本不好控制,对于不熟练的开发者来说,很容易写错地方,导致crash。应对这样的情况,系统给我们提供了另一个锁NSConditionLock条件锁

3.2 NSConditionLock条件锁

相关概念
  • NSConditionLock条件锁,一旦一个线程获得锁,其他线程一定等待。
  • [xxxx lock];表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁。
  • [xxx lockWhenCondition:A条件];
    • 如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待
    • 如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
  • [xxx unlockWithCondition:A条件]; 表示释放锁,同时把内部的condition设置为A条件
  • return = [xxx lockWhenCondition:A条件 beforeDate:A时间]; 表示如果被锁定(没获得锁),并超过该时间不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理
  • 所谓的condition就是整数,内部通过整数比较条件。
案例分析
- (void)lg_testConditonLock{
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [conditionLock lockWhenCondition:1];
        NSLog(@"线程 1");
        [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        [conditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [conditionLock lock];
        NSLog(@"线程 3");
        [conditionLock unlock];
    });
}

run👇

分析:

  • 线程1 调用[NSConditionLock lockWhenCondition:],因为不满足当前条件,所
    以会进入 waiting 状态,会释放当前的互斥锁。线程2同理。
  • 此时当前的线程3 调用[NSConditionLock lock:],本质上是调用[NSConditionLock lockBeforeDate:],这里不需要比对条件值,所以线程3被优先打印
  • 接下来线程2 执行[NSConditionLock lockWhenCondition:],此时满足条件值,所以线程 2被打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将
    value 设置为 1,并发送 boradcast,此时线程1接收到当前的信号,唤醒执行并打印。

所以,当前打印为 线程 3 -->线程 2 --> 线程 1

条件锁底层源码

下面,我们依旧看看swift版的NSConditionLock的源码👇

接着看看[NSConditionLock lockBeforeDate:]源码👇

最后看看unlockWithCondition:源码👇

总结

开篇我们举出了日常使用的各种锁的性能对比,然后重点分析了@synchronize的底层原理,然后通过多线程递归示例的演示,接着分析了NSLock和递归锁NSRecursiveLock的底层,最后通过经典的生产消费者模型示例,分析了条件变量NSCondition和 条件锁NSConditionLock的底层源码处理流程。

补充:读写锁

读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁

#include <pthread.h>
// 成功则返回0, 出错则返回错误编号
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)

同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源。

// 获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 释放锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

注意:获取锁的两个函数是阻塞操作。

当然,那有没有非阻塞的函数👇

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.

读写锁示例

请参考XFReadWriteLocker,不对的地方请不吝赐教!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352