对iOS中几种锁的理解

概述:

在程序编程中,很多地方会涉及到多线程操作的编程。而在多线程中,就不得不说说其中"锁"的存在。多线程开发是为了发挥多核CPU的优势和防止因为单个线程的阻塞而造成整个系统的阻塞。而锁就是为了解决多线程在同时访问同一块资源时保持同步的方式。

常见的八大锁:

  1. OSSpinLock 自旋锁
  2. dispatch_semaphore 信号量实现加锁(GCD)
  3. pthread_mutex 互斥锁(C语言)
  4. NSCondition
  5. NSConditionLock 条件锁
  6. NSRecursiveLock 递归锁
  7. NSLock 对象锁
  8. @synchronized 关键字加锁

原理简介

1. OSSpinLock

OSSpinLock的一种自旋锁。原理是以一个标志位来标记该资源当前是否处在锁住(lock)的状态,如果是锁住(lock)的状态,那就会执行(do - while)死循环,这个过程称作忙等(busy-wait)。直到该资源被解锁(unlock),那么值钱一直在忙等状态下的线程,就会跳出循环,获得该资源并同时加锁(lock)。

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

上图中的伪代码,其实还存在一个问题:那就是如果一开始的时候同时有多个多线程执行 while 循环判断,此时它们拿到的标记位都是false,都不会被进入执行死循环进入忙等状态。这样就没法保证了锁的可靠性了。解决思路其实也是跟加锁思路一样,就是让申请锁的这一步为原子操作。保证此时只有一个线程在申请锁。

自旋锁的效率虽然很高,但是在有的场景下,并不是第一选择。比如在某些比较耗时的操作中,此时其它线程一直在做while循环操作,处于忙等状态。最终可能因为超时被系统释放,进入睡眠状态。显然中间忙等的过程被白白浪费了,是没有必要的。

OSSpinLock在特定情况下会出现优先级反转的问题,具体可以阅读ibireme大神的不再安全的 OSSpinLock的文章。笔者在这里简单概述一下:
在iOS系统中,苹果为了方便管理多线程,把多线程分为了5个程度的优先级:background,utility,default,user-initiated,user-interactive。并规定高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法就会产生潜在的优先级反转问题:
当一个低优先级的线程申请得到锁,并准备开始执行的时候,此时另一个高优先级的线程在循环中尝试申请获得锁,处于忙等状态中并占用大量的CPU时间。而此时获得锁的低优先级线程没法跟高优先级线程争夺CPU时间,无法完成任务、无法释放锁。
在iOS10以上苹果推出了 os_unfair_lock_t 来替代 OSSpinLock

image.png

2. dispatch_semaphore

dispatch_semaphore是在GCD中的一种同步方式,采用信号量的实现原理。主要有三个函数:

  1. dispatch_semaphore_create(long value):
    创建一个dispatch_semaphore_t 类型的信号量,且值为value。注意此处传入的value必须大于0,否则会返回null。
  2. dispatch_semaphore_signal(dispatch_semaphore_t signal)
    参入一个dispatch_semaphore_t 类型的信号量,并且使传入的信号量加1
  3. dispatch_semaphore_wait(dispatch_semaphore_t signal, dispatch_time_t timeout)
    参数是一个信号量和一个超时时间。函数内部会判断传入的信号量的值如果大于0,会继续执行下面的代码,并且将传入的信号量减1。如果函数内部判断传入的信号量的值等于0,则会进入waiting状态,主动让出时间片,如果在wai的过程中,其它信号发送了信号(执行了dispatch_semaphore_signal函数),就会继续执行下面的任务。如果在wait的过程中一直没有收到信号直到timeOut,也会继续执行下面的任务。看起来似乎跟NSLock一样,区别在于dispatch_semaphore保存了信号量这个参数,如果初始化设置信号量为x,那么就会同时有x个线程同时访问受保护区。而NSLock只能同一时间一个线程可以访问受保护区。
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
    dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(signal, timeOut);
        sleep(2);
        NSLog(@"操作1");
        dispatch_semaphore_signal(signal);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(signal, timeOut);
        sleep(1);
        NSLog(@"操作2");
        dispatch_semaphore_signal(signal);
    });

3. pthread_mutex

互斥锁,原理和信号量相似。遇到锁住的情况,会主动让出时间片,进入休眠状态,等待上下文切换唤醒。所以这种方式通常适合用在等待时间较长的场景下。因为系统上下文切换唤醒线程需要时间操作,通常在10微妙左右。如果等待时间比上下文切换的时间还短,使用忙等操作(busy-wait)比主动让出时间片进入睡眠更高效。
pthread_mutex有三种类型的锁:

  • PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
  • PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
  • PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。lock和unlock需要一一对应
  • PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
pthread_mutexattr_init(&attr);  
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
 
pthread_mutex_t mutex;  
pthread_mutex_init(&mutex, &attr) // 创建锁
 
pthread_mutex_lock(&mutex); // 申请锁  
    // 临界区
pthread_mutex_unlock(&mutex); // 释放锁 

4.@synchronize

这可能是我们用的最多的一种,写法简单,但是性能是最差的。atomic内部在setter和getter方法中就使用了它,使用它的时候,需要传入一个对象作为标记,底层实现原理是OC的runtime会为这个对象创建一个递归锁并存储在哈希表中,key就是该对象。传入的对象如果是nil的话,则不会得到任何锁,不会起到线程安全的作用。

4.其它

  1. NSLock实际上是在pthread_mutex基础上进行了封装了,且pthread_mutex的类型为 PTHREAD_MUTEX_ERRORCHECK,会有错误提示,同时会损失一定性能。
  2. NSRecursiveLock : 内部封装了 pthread_mutex ,类型为 PTHREAD_MUTEX_RECURSIVE。
  3. NSCondition :封装了一个互斥锁(pthread_mutex)和一个条件变量。
  4. NSConditionLock : 在NSCondition的基础上来实现的,内部持有一个NSCondition对象和一个condition_value属性。

参考文章:

ibiremed的不再安全的OSSpinLock
自旋锁和互斥锁的理解

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,514评论 0 6
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 656评论 0 0
  • 转载自:https://www.jianshu.com/p/938d68ed832c# 一、前言 前段时间看了几个...
    cafei阅读 4,536评论 1 12
  • 在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? ...
    IAMCJ阅读 3,092评论 2 25
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 789评论 0 2