iOS 各种锁

概述

iOS多线程开发,会出现数据竞争,因此需要锁来保证线程安全。


iOS锁

线程安全

当一个线程访问资源时,需要保证其它的线程不能访问同一资源,即同一时刻只能有一个线程对数据进行操作。因此需要锁来保证线程的安全。

锁的使用步骤

  1. 准备一把锁:递归锁、互斥锁、条件锁、信号量等等
  2. 线程1中:
    • 加锁(- (void)lock;
    • 处理数据
    • 解锁(- (void)unlock;
  3. 线程2中:
    • 等待锁在线程1中使用完毕,并解锁
    • 加锁(- (void)lock;
    • 处理数据
    • 解锁(- (void)unlock;

从原理上讲锁分为哪几种?

  1. 自旋锁(叫循环判断锁更恰当一些)

    已加锁时一直循环处于判断 lock 状态,如果 lock = 0 接着循环,如果 lock = 1 跳出循环转去临界区。因此,如果别的线程长期持有该锁,那么这个线程就一直在 while 地检查是否能够加锁,浪费 CPU 做无用功;如果拥有锁后很快释放锁,那么自旋锁就比较高效。一般用于多核 CPU

    while 抢锁(lock) == 没抢到 {
    }
    
  2. 互斥锁

    已加锁时阻塞掉当前线程,让出 CPU 资源,通过减少 CPU 的浪费来提高效率。操作系统负责线程调度,为了实现“锁的状态发生改变时再唤醒”,就需要把锁也交给操作系统管理。所以这个过程需要进行上下文的切换,保存寄存器状态需要花费时间,操作花销较自旋锁更大

    while 抢锁(lock) == 没抢到 {
     本线程先去睡了,请在这把锁的状态发生改变时再唤醒
    }
    
  3. 读写锁(共享-独占锁)

    是一种读共享,写独占的锁。主要有两种特性:

    • 当读写锁被加了写锁时,其他线程对该锁加度锁或者写锁都会阻塞(不是失败)
    • 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功

iOS 开发中常用的几种锁分别属于哪种类型?

自旋锁
  • os_unfair_lock

  • OSSPinLock:iOS 10.0 以后被弃用

互斥锁
  • pthread_mutex:有 PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE 三种类型
  • NSLock:对 mutex 普通锁的封装
  • NSRecursiveLock:对 mutex 递归锁的封装
  • NSCondition:对 mutex 和 条件的封装
  • NSConditionLock: 对 NSCondition 的进一步封装,可以设置具体的条件
  • 信号量 dispatch_semaphore:如果信号量不小于 0,立刻返回,否则是线程睡眠,让出时间片,导致操作系统切换到另一个线程,会花费 10us左右时间,并且需要切换两次
  • @synchronized:是对 mutex 递归锁的封装,@sychronized(obj) 内部会生成 obj 对应的递归锁,然后进行加锁、解锁操作
  • atomic:用于保证属性的 setter、getter 的原子属性操作,相当于在 getter 和 setter 内部加了线程同步的锁。它不能保证使用属性的过程是线程安全的
读写锁
  • pthread_rwlock:需要注意以下场景
    1. 同一时间,只能有 1 个线程进行写操作
    2. 同一时间,允许有多个线程进行多操作
    3. 同一时间,不允许既有写操作,又有读操作

常用的锁

开发中常用如下几种锁

NSRecursiveLock:递归锁

NSLock多次 lock却没有unlock会导致死锁,例如下列情形,在递归调用中,会出现死锁。

self.myLock = [NSLock new];
dispatch_async(self.queue_1, ^{
        static void (^recursiveBlock)(int);
        recursiveBlock = ^(int value) {
            [self.myLock lock];
            if (value > 0) {
                recursiveBlock(value - 1);
            }
            [self.myLock unlock];
        };
        recursiveBlock(10);
    });

因此有了递归锁(NSRecursiveLock),将上述代码中的NSLock换成NSRecursiveLock即可解决问题。递归锁其他用法与NSLock一致。如下代码所示

dispatch_async(self.queue_1, ^{
        static void (^recursiveBlock)(int);
        recursiveBlock = ^(int value) {
            [self.recursiveLock lock];
            if (value > 0) {
                recursiveBlock(value - 1);
            }
            [self.recursiveLock unlock];
        };
        recursiveBlock(10);
    });
    dispatch_async(self.queue_2, ^{
        BOOL x = [self.recursiveLock lockBeforeDate:[NSDate distantFuture]];
        if (x) {
            [self.recursiveLock unlock];
        } else {
            NSLog(@"线程__2:__获取__锁__失败");
        }
    });
NSConditionLock:条件锁

NSConditionLock相比于NSLock多了一个条件
当两个线程需要根据特定的条件或者按照特定的顺序执行时,就需要NSConditionLock。例如代码中开启了线程一下载图片,线程二处理图片。只有线程一下载图片完成后,线程二才能开始处理图片。在线程一下载完成之前,线程二处于阻塞状态。

self.conditionLock = [[NSConditionLock alloc] initWithCondition:kConditionOne];
dispatch_async(self.queue_1, ^{
        [self.conditionLock lockWhenCondition:kConditionOne];
        sleep(5);
        [self.conditionLock unlockWithCondition:kConditionTwo];
    });
    dispatch_async(self.queue_2, ^{
        [self.conditionLock lockWhenCondition:kConditionTwo];
        sleep(5);
        [self.conditionLock unlock];
    });
  • 条件锁在初始化的时候,设定了一个条件kConditionOne
  • [self.conditionLock unlockWithCondition:kConditionTwo];解锁时重新给NSConditionLock设定了一个条件为kConditionTwo
  • 当满足条件kConditionTwo时,可以重新获取这把锁
  • 最后不需要更改获取锁的条件了,直接解锁

NSCondition

NSCondition是一种特殊的锁,与NSConditionLock类似,但是实现方式不一样。

dispatch_async(self.queue_1, ^{
        NSLog(@"线程__1:__准备获取__锁__");
        [self.myCondition lock];
        NSLog(@"线程__1:__获取__锁__成功,并开始等待");
        [self.myCondition wait];
        NSLog(@"线程__1:__结束等待");
        [self.myCondition unlock];
        NSLog(@"线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"线程__2:__准备获取__锁__");
        [self.myCondition lock];
        NSLog(@"线程__2:__获取__锁__成功,并开始等待");
        [self.myCondition signal];
        NSLog(@"线程__2:__发出信号");
        [self.myCondition unlock];
        NSLog(@"线程__2:__解__锁__成功");
    });

上述代码运行结果


NSCondition

其中

  • - (void)wait;会阻塞当前线程
  • - (void)signal;激活一个阻塞的线程
  • - (void)broadcast;激活所有阻塞的线程
    备注:
    • 通过测试发现,- (void)signal;按照调用- (void)wait;方法先后顺序激活线程,并不是首先激活与调用- (void)signal;方法的线程
    • 可以多次调用- (void)signal;方法依次激活被- (void)wait;阻塞的线程
dispatch_semaphore:信号量

信号量类似于自动取款机。一次只能有一个人使用取款机。如果来的人多了,只能在旁边等着,如果使用取款机的人办完业务了,下一个人才能继续使用。

  • dispatch_semaphore_create(1)传入值需>=0,若传入0,则阻塞线程
  • dispatch_semaphore_wait(semaphore, timeout);,等待timeout,到了时间后会继续执行代码;或者信号量semaphore大于0也会继续执行代码
  • dispatch_semaphore_signal(signal);类似于unlock,信号量会+1
    信号量原理:首先把信号量减一,如果不小于零,就立刻返回,否则就使线程睡眠,让出时间片。主动让出时间片会导致操作系统切换到另一个线程,会花费10us左右的时间,并且需要切换两次,因此如果忙等时间只有几微秒,忙等比线程睡眠更高效。

如下述代码所示。

dispatch_semaphore_t signal = dispatch_semaphore_create(1);

dispatch_queue_t queue1 = dispatch_queue_create("globalQueue1", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    NSLog(@"线程1:等待");
    dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"线程1:启动");
    dispatch_semaphore_signal(signal);
    NSLog(@"线程1:新的信号");
});

实例:有三个任务异步A、B、C,其中需要在A、B执行完毕后才可以执行任务C
可以使用信号量解决:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"任务A执行");
    dispatch_semaphore_signal(semphore);
});
dispatch_async(queue, ^{
    sleep(3);
    NSLog(@"任务B执行");
    dispatch_semaphore_signal(semphore);
    });
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER); dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"C执行等待");
POSIX互斥锁

POSIX互斥锁是Linux/Unix平台上提供的API,C语言级别的锁,使用POSIX互斥锁需要引入头文件#import <pthread.h>,并申明初始化一个pthread_mutex_t的结构。使用完毕,需要在- (void)dealloc;中释放锁。

  • pthread_mutex_lock加锁
  • pthread_mutex_unlock解锁
  • pthread_mutex_destroy释放锁
    原理:pthread_mutex与信号量原理类似,不使用忙等,而是阻塞线程并睡眠,需要上下文切换,有多种类型,可通过定义锁的属性PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE确定类型。
    互斥锁内部会首先判断锁的类型,所以效率相对于信号量会低一些。
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
    
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr); // 创建锁

示例如下

dispatch_async(self.queue_1, ^{
        NSLog(@"线程__1:__准备获取__锁__");
        pthread_mutex_lock(&_mutex);
        NSLog(@"线程__1:__获取__锁__成功");
        sleep(5);
        pthread_mutex_unlock(&_mutex);
        NSLog(@"线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"线程__2:__准备获取__锁__");
        pthread_mutex_lock(&_mutex);
        NSLog(@"线程__2:__获取__锁__成功");
        pthread_mutex_unlock(&_mutex);
        NSLog(@"线程__2:__解__锁__成功");
    });
pthread_mutex_destroy(&_mutex);

结果如下

POSIX互斥锁

备注:POSIX提供了一整套完整的API,功能强大,非常底层。

NSLock:最基本的锁
  • - (void)lock;
  • - (void)unlock;
  • - (BOOL)lockBeforeDate:(NSDate *)limit;在limit时间内尝试获取锁,为获取锁前一直阻塞线程,例如10s,如果10s内的时间,其它线程释放了锁(unlock),该方法会立刻获取锁
    原理:NSLock内部封装了属性为PTHREAD_MUTEX_ERRORCHECKpthread_mutex,由于存在方法调用,因此会比pthread_mutex更慢
    注意:
    1. lockunlock方法必须在同一线程中执行
    2. 连续lock中间没有unlock会引起死锁
dispatch_async(self.queue_1, ^{
        NSLog(@"线程1:等待");
        [self.myLock lock];
        NSLog(@"线程1");
        sleep(5);
        [self.myLock unlock];
        NSLog(@"线程1:解锁成功");
});

dispatch_async(self.queue_2, ^{
        NSLog(@"线程2:等待");
        BOOL x = [self.myLock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:6]];
        //BOOL x = [self.myLock lockBeforeDate:[NSDate distantFuture]];
        if (x) {
            NSLog(@"线程2:成功");
            [self.myLock unlock];
        } else {
            NSLog(@"线程2:失败");
        }
});
@synchronized

具体可以参考关于@synchronized 比你想知道的还多@synchronized(objc)会在运行时为 objc 分配递归锁,如果objc 为nil,则代码失去了线程安全性,如果在 @synchronized(objc)中 objc 惜构,也不会产生问题
具体代码如下

dispatch_async(self.queue_1, ^{
        NSLog(@"synch__线程__1:__准备获取__锁__");
        @synchronized(self) {
            NSLog(@"synch__线程__1:__获取__锁__成功");
            sleep(5);
        }
        NSLog(@"synch__线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"synch__线程__2:__准备获取__锁__");
        @synchronized(self) {
            NSLog(@"synch__线程__2:__获取__锁__成功");
        }
        NSLog(@"synch__线程__2:__解__锁__成功");
    });

结果:


synchronized
OSSpinLock:自旋锁

由于OSSpinLock存在优先级反转问题,在iOS 10.0被os_unfair_lock代替了


os_unfair_lock

在iOS 10.0后可用,用于代替自旋锁。使用时需要引入头文件#import <os/lock.h>

static os_unfair_lock unfairLock;
unfairLock = OS_UNFAIR_LOCK_INIT;
dispatch_async(self.queue_1, ^{
        NSLog(@"unfairLock__线程__1:__准备获取__锁__");
        os_unfair_lock_lock(&unfairLock);
        NSLog(@"unfairLock__线程__1:__获取__锁__成功");
        sleep(5);
        os_unfair_lock_unlock(&unfairLock);
        NSLog(@"unfairLock__线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
        NSLog(@"unfairLock__线程__2:__准备获取__锁__");
        os_unfair_lock_lock(&unfairLock);
        NSLog(@"unfairLock__线程__2:__获取__锁__成功");
        os_unfair_lock_unlock(&unfairLock);
        NSLog(@"unfairLock__线程__2:__解__锁__成功");
});
结果

性能分析

,性能如下:

  1. 测试环境:iPhone SE,iOS 10.0.1


    iPhone SE上锁性能定性分析
  2. 测试环境:iPhone 7, iOS 11.4


    iPhone 7上锁性能分析

上述图表是在单线程下测试的,并且只测试了加锁、解锁的性能,因此只能做定性分析。
从图表中可以看出,dispatch_semaphore性能最好,p thread_mutex、os_unfair_lock、NSCondition性能相近,pthread_mutex_recursive、NSRecursiveLock、NSLock性能次之,然后是NSConditionLock,synchronized性能最差。

未完待续

参考

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,503评论 0 6
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 644评论 0 0
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 777评论 0 2
  • 转载自:https://www.jianshu.com/p/938d68ed832c# 一、前言 前段时间看了几个...
    cafei阅读 4,531评论 1 12
  • 一、前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized、NSLock...
    稻春阅读 464评论 0 0