ios 锁的种类及性能

一、基本概念

ios中的锁主要可以分为两大类,互斥锁自旋锁,其他锁都是这两种锁的延伸和扩展。

1、介绍

互斥锁:属于sleep-waiting类型的锁,线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

自旋锁:线程A获取到锁,在释放锁之前,线程B又来获取锁,此时获取不到,线程B就会不断的进入循环,一直检查锁是否已被释放,如果释放,则能获取到锁。

2、区别

互斥锁:当线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时,线程会被唤醒,同时获取到锁,继续执行任务,互斥锁会改变线程的状态。线程从sleep(加锁)—>running(解锁)的过程中,有上下文的切换,cpu的抢占,信号的发送等开销。

自旋锁:当线程获取锁但没获取到时,不会进入休眠,而是一直循环,线程始终处于活跃状态,不会改变线程状态,也就是忙等。线程一直是running(加锁—>解锁),死循环检测锁的标志位。递归调用自旋锁一定会死锁

对比:互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

3、使用场景

  • 互斥锁会改变线程的状态,使得内核不断的调度线程资源,因此效率上比自旋锁要低很多,不适合使用自旋锁的场景都使用互斥锁。
  • 自旋锁在线程的等待过程中是活跃的,避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此自旋锁适合用于短时间内的轻量级锁定,主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。

二、锁的分类

2.1 NSLock

NSLock「非」递归互斥锁

NSLocking只定义了加锁(获取锁)-lock,和解锁(释放锁)-unlock两个接口。NSLockNSConditionLockNSRecursiveLockNSCondition都实现了这个协议

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

-lock-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用,否则会产生未知结果。参考:官方文档原文
NSLock是使用了pthread_mutex_t封装的互斥锁。

2.2 NSCondition

NSCondition也是使用了pthread_mutex_t封装的互斥锁,和NSLock中一模一样,同时还使用了pthread_cond_t。它和NSLock的区别是:

  • NSLock在获取不到锁的时候自动使线程进入休眠,锁被释放后线程又自动被唤醒
  • NSCondition可以使我们更加灵活的控制线程状态,在任何需要的时候使线程进入休眠或唤醒它。
    [condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
    [condition unlock]:与lock 同时使用
    [condition wait]:让当前线程处于等待状态
    [condition signal]:CPU发信号告诉线程不用在等待,可以继续执行

2.3 NSConditionLock

NSConditionLock条件锁就是有特定条件的锁,说白了就是「有条件的互斥锁」

  1. 只读属性condition,保存锁当前的条件(所谓的条件condition就是个NSInteger
  2. -lockWhenCondition::获取锁,如果condition与属性相等,则可以获得锁,否则阻塞线程,等待被唤醒
  3. -unlockWithCondition:释放锁,并修改condition属性

基本用法:

// 主线程
    self.conditionLock = [[NSConditionLock alloc] init];
        
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"进入线程1");
        // 当 lock.condition = 2 时,能够获取到锁,否则休眠等待
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"执行任务1");
        sleep(1);
        [self.lock unlock];
        NSLog(@"退出线程1");
    });
        
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"进入线程2");
        [self.conditionLock lockWhenCondition:1];
        NSLog(@"执行任务2");
        sleep(5);
        // 将 lock.condition 修改为2,线程1就能获得锁了
        [self.conditionLock unlockWithCondition:2];
        NSLog(@"退出线程2");
    });
        
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"进入线程3");
        [self.lock lock];
        NSLog(@"执行任务3");
        sleep(2);
        // 将 lock.condition 修改为1,线程2就能获得锁了
        [self.conditionLock unlockWithCondition:1];
        NSLog(@"退出线程3");
    });

打印结果:

2020-04-24 16:20:44.901816+0800 lock[48829:11985930] 进入线程3
2020-04-24 16:20:44.901816+0800 lock[48829:11985929] 进入线程2
2020-04-24 16:20:44.901860+0800 lock[48829:11985931] 进入线程1
2020-04-24 16:20:44.902052+0800 lock[48829:11985930] 执行任务3
2020-04-24 16:20:46.906596+0800 lock[48829:11985930] 退出线程3
2020-04-24 16:20:46.906618+0800 lock[48829:11985929] 执行任务2
2020-04-24 16:20:51.908623+0800 lock[48829:11985931] 执行任务1
2020-04-24 16:20:51.908629+0800 lock[48829:11985929] 退出线程2
2020-04-24 16:20:52.913340+0800 lock[48829:11985931] 退出线程1
  • [xxx lockWhenCondition:A条件]:表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并 且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码 的完成,直至它解锁。
  • -tryLockWhenCondition:,即使在未来某个时间点可以满足条件,它只根据当前condition获取锁,无论能否获取到锁,该线程都会继续向下执行,不会阻塞
  • -lockWhenCondition:beforeDate: 就是要在超时之前,并且满足条件才能获取到锁,返回YES,否则返回NO。同时如果被锁定(已获得锁),并超过该时间也不再阻塞线程。

NSCondition使用了一个单一条件,而NSConditionLock似乎把这个条件暴露为参数给我们使用,并且可以是不同的条件。

2.4 递归锁NSRecursiveLock

NSRecursiveLock互斥锁中的递归锁,可被 同一线程多次获取,而不会产生死锁。什么意思呢,一个线程已经获得了锁,开始执行受锁保护的代码(锁还未释放),如果这段代码调用了其他函数,而被调用的函数又要获取这个锁,此时已然可以获得锁并正常执行,而不会死锁。
基本用法:

- (void)testLock{
    self.lock = [[NSRecursiveLock alloc] init];
    [NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];
    [NSThread detachNewThreadSelector:@selector(testLock3) toTarget:self withObject:nil];
}

- (void)testLock1 {
    [self.lock lock];
    NSLog(@"testLock1");
    [self testLock2];
    [self.lock unlock];
    NSLog(@"testLock1: unlock");
}

- (void)testLock2 {
    [self.lock lock];
    NSLog(@"testLock2");
    [self.lock unlock];
    NSLog(@"testLock2: unlock");
}

- (void)testLock3 {
    [self.lock lock];
    NSLog(@"testLock3: %@", [NSThread currentThread]);
    [self.lock unlock];
    NSLog(@"testLock3: unlock");
}

NSLock相比,NSRecursiveLock也实现了NSLocking协议,并定义了两个方法,这些接口的使用和NSLock是完全相同的。唯一不同的就是NSRecursiveLock递归调用,而NSLock如果想上面描述的场景使用的话就会死锁。
实际上NSRecursiveLock可以递归,只是给锁设置了一个递归属性,具体的实现是在pthread中实现的.有兴趣的童鞋可以研究下pthread或者POSIX

2.5 对象锁/同步锁 @synchronized

@synchronized(id)的使用应该是较多的,它底层实现是个递归锁,不会产生死锁,且不需要手动去加锁解锁,使用起来比较方便。

2.6 atomic

atomic用于保证属性settergetter的原子性操作,相当于在gettersetter内部加了线程同步的锁。并不能保证使用属性的过程是线程安全的。
简单验证:

@property (atomic) NSInteger number;

- (void)testAtomic{
    
    // 线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 1000; i++) {
            self.number = self.number + 1;
            NSLog(@"number: %ld", self.number);
        }
    });
    
    // 线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 1000; i++) {
            self.number = self.number + 1;
            NSLog(@"number: %ld", self.number);
        }
    });
    
}

atomic修饰,理论上是线程安全的,但是最终打印的结果却不是2000
这是因为两个线程在并发的调用settergetter,在setter和getter内部是加了锁,但是在做+1操作的时候并没有加锁,导致在某一时刻,线程一调用了getter取到值,线程2恰好紧跟着调用了getter,取到相同的值,然后两个线程对取到的值分别+1,再分别调用setter,使得两次setter其实赋值了相等的值。
也就是说:atomic只能保证settergetter的安全,并不是绝对的线程安全。

2.7 dispatch_semaphore

信号量semaphore是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
  • GCD的信号量是对系统内核信号量的一层封装,要想更深入的了解,可以去研究一下Linux内核的信号量。
//如果信号量的初始值为负,是不正确的
//表示最多开启value个线程
dispatch_semaphore_create(long value)
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
// DISPATCH_TIME_FOREVER线程堵塞等待时间,该值表示一直等待;若为1,则表示超时等待1s,1s后继续执行代码
// 返回值,如果,==0表示线程未超时; >0,线程超时。
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

2.8 读写锁 pthread_rwlock

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为 在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁。正是因为这个特性pthread_rwlock 适合于对数据结构读次数比写次数多得多的情况。因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。
使用时需要导入头文件#import <pthread.h>,iOS中的读写安全方案需要注意一下场景:

  1. 同一时间,只能有1个线程进行写的操作
  2. 同一时间,允许有多个线程进行读的操作
  3. 同一时间,不允许既有写的操作,又有读的操作

2.9 OSSpinLock & os_unfair_lock

OSSpinLock 因为存在安全问题,在ios10之后已经被弃用。os_unfair_lock(#import <os/lock.h>)用于取代不安全的OSSpinLock,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

2.10 pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。需要导入头文件#import <pthread.h> 使用步骤。
简单使用:

@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;

#pragma mark - mutex

- (void)testMutex{
    
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(&_mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);

    // 初始化条件
    pthread_cond_init(&_cond, NULL);

    self.data = [NSMutableArray array];

    
    [self otherTest];
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeData) object:nil] start];

    [[[NSThread alloc] initWithTarget:self selector:@selector(addData) object:nil] start];
}

// 线程1
// 删除数组中的元素
- (void)removeData{
    pthread_mutex_lock(&_mutex);
    NSLog(@"remove - begin");

    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }

    [self.data removeLastObject];
    NSLog(@"删除了元素");

    pthread_mutex_unlock(&_mutex);
}

// 线程2
// 往数组中添加元素
- (void)addData{
    pthread_mutex_lock(&_mutex);

    sleep(1);

    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");

    // 激活一个等待该条件的线程
    pthread_cond_signal(&_cond);
    
    pthread_mutex_unlock(&_mutex);
    NSLog(@"add - end");
}

- (void)dealloc{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);

}

PTHREAD_MUTEX_RECURSIVEPTHREAD_MUTEX_DEFAULT,这两个属性会创建不同类型的锁,当然也会有不同的打印结果。

三、锁的关系及性能

锁的分类及关系:
锁的分类及关系.png

性能比较(从高到低排序):

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,503评论 0 6
  • 前言 在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。...
    赵梦楠阅读 903评论 0 5
  • 引用自多线程编程指南应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两个线程同时修改同一资源有...
    Mitchell阅读 1,973评论 1 7
  • 翻译:Synchronization 同步 应用程序中存在多个线程会导致潜在的问题,这些问题可能会导致从多个执行线...
    AlexCorleone阅读 2,441评论 0 4
  • 转自(https://bestswifter.com/ios-lock/#) 深入理解 iOS 开发中的锁 摘要 ...
    犯色戒的和尚阅读 315评论 0 1