iOS中的锁

起底多线程同步锁(iOS)

OSSpinLock

NSLock

NSRecursiveLock

同步

NSCondition

iOS/MacOS为多线程、共享内存(变量)提供了多种的同步解决方案(即同步锁),对于这些方案的比较,大都讨论了锁的用法以及锁操作的开销,然后就开销表现排个序。春哥以为,最优方案的选用还是看应用场景,高频接口PK低频接口、有限冲突PK激烈竞争、代码片段耗时的长短,以上都是正确选用的重要依据,不同方案在其适用范围表现各有不同。这些方案当中,除了熟悉的iOS/MacOS系统自有的同步锁,另外还有两个自研的读写锁,还有应用开发中常见的set/get访问接口的原子操作属性。

1、@synchronized(){}

Objective-C同步语法能够实现对block内的代码片段加锁, 可以指定任意一个Objective-C对象(id指针)作为锁“标记”,该语法将“标记”理解为token;

2、NSLock、NSRecursiveLock:

典型的面向对象的锁,即同步锁类,遵循Objective-C的NSLocking协议接口,前者支持tryLock,后者支持递归(可重入);

3、NSCondition、NSConditionLock:

基于信号量方式实现的锁对象,前者提供单独的信号量管理接口,相比后者用法上可以更为灵活,而后者在接口上更为直接、实用;

4、ANReadWriteLockANRecursiveRWLock

iOS/MacOS并没有提供读写锁,春哥尝试自己搞,Objective-C版的读写锁(ANLock),遵循读写锁特性,前者写锁耗时较小,后者支持递归;

5、pthread_mutex:

POSIX标准的unix多线程库(pthread)中使用的互斥量,支持递归,需要特别说明的是信号机制pthread_cond_wait()同步方式也是依赖于该互斥量,pthread_cond_wait()本身并不具备同步能力;

6、dispatch_semaphore:

GCD用于控制多线程并发的信号量,允许通过wait/signal的信号事件控制并发执行的最大线程数,当最大线程数降级为1的时候则可当作同步锁使用,注意该信号量并不支持递归;

7、OSSpinLock:

iOS/MacOS自有的自旋锁,其特点是线程等待取锁时不进内核,线程因此不挂起,直接保持空转,这使得它的锁操作开销降得很低,OSSpinLock是不支持递归的;

8、atomic(property) set/get:

利用set/get接口的属性实现原子操作,进而确保“被共享”的变量在多线程中读写安全,这已经是能满足部分多线程同步要求;

基础表现-锁操作耗时:

上图是常规的锁操作性能测试(iOS7.0SDK,iPhone6模拟器,Yosemite 10.10.5),垂直方向表示耗时,单位是秒,总耗时越小越好,水平方向表示不同类型锁的锁操作,具体又分为两部分,左边的常规lock操作(比如NSLock)或者读read操作(比如ANReadWriteLock),右边则是写write操作,图上仅有ANReadWriteLock和ANRecursiveRWLock支持,其它不支持的则默认为0,图上看出,单从性能表现,原子操作是表现最佳的(0.057412秒),@synchronized则是最耗时的(1.753565秒) (测试代码)

正如前文所述,不同方案各有侧重,适用于不用的场景,不能唯性能论高低:

原子操作虽然性能很好,但仅限于set/get,比如对列表的插入移除操作需要做同步则无能为力,支持不到,所以适用于一些实例成员变量的读写同步;

得益于不进内核不挂起的方式,OSSpinLock有着优异的性能表现,然而在高并发执行(冲突概率大,竞争激烈)的时候,又或者代码片段比较耗时(比如涉及内核执行文件io、socket、thread等),就容易引发CPU占有率暴涨的风险,因此更适用于一些简短低耗时的代码片段;

上图为OSSpinLock等待取锁时的耗时测试用例代码,下图为测试结果,图中可以看到,等待取锁时,如果异步线程比较耗时,CPU占有率会有一个飙升 (测试代码)

dispatch_semaphore的性能表现出乎意料之外的好,也没有OSSpinLock的CPU占有率暴涨的问题,然而原本是用于GCD的多线程并发控制,也是信号量机制,是否适用于常规同步锁有待实践验证,春哥这里仅提供选择,不做推荐;

上图为dispatch_semaphore测试用例

pthread_mutex是pthread经典的基于互斥量机制的同步锁,特性、性能以及稳定各方面都已被大量项目所验证,也是春哥比较推荐作为常规同步锁首选;

上图为pthread_mutex用法举例

读写锁的在锁操作耗时上明显不占优势,读写锁的主要性能优势在于多线程高并发量的场景,这时候锁竞争可能会非常激烈,使用一般的锁这时候并发性能都会明显下降,读写锁对于所有读操作能够把同步放开,进而保持并发性能不受影响;以pthread_mutex和ANRecursiveRWLock为例,假设mutex的lock耗时为lk,则rw的read

lock耗时为2.7lk(从性能测试图表数据得出),read操作耗时为rd,1000次的多线程接口访问:

mutex总耗时 = 1000*lk + 1000*rd

rw总耗时 = 1000*2.7*lk + 1000/c*rd

其中c表示应用的并发数,根据开发文档和技术资料,iOS第二条线程起stack为512KB,而单个应用useable memory size在50MB以内,即c<=100;

假设线程数取中值c=50(严格来说,线程数不等于冲突计数,冲突计数很可能会比线程数小得多,线程同步运行不代表就即刻会发生冲突),当 mutex总耗时 > rw总耗时:

mutex总耗时 > rw总耗时  =》 50*lk + 50*rd > 50*2.7lk + rd  =》 49*rd > 85*lk   =》 rd > 1.73*lk

可以看出,只要read操作耗时超过锁操作耗时的1.7倍(这其实很容易达到的),读写锁的性能就会占优势

假设线程数c=2(如上述,这里是假设了两个线程之间是竞争了,发生冲突,实际未必):

mutex总耗时 > rw总耗时  =》 2*lk + 2*rd > 5.4*lk + rd  =》 rd > 3.4lk

即使只有两个并发线程,只要read操作耗时超过锁操作耗时的3.4倍,读写锁的性能还会占优势

假设线程数c=1:

mutex总耗时 > rw总耗时  =》0 > 1.7lk

这显然不成立,说明当单个线程的时候,rw的性能不可能有优势。这也好理解,这时候的mutex和rw的读操作都相当完全同步,不论是mutex还是rw,性能完全取决于锁操作本身,而rw在锁操作耗时上就不占优势,所以mutex总耗时总是要小于rw总耗时的。

上图是mutex锁和rw锁read操作的耗时测试用例,下图为测试结果,read操作设置为100微秒,mutex锁的总耗时是rw锁的5倍多,read操作的耗时远比锁操作大许多(2k倍),根据上述恒等式计算可以得出实际的冲突计数c=5 (测试代码)

其它方案的讨论:

a、NSCondition和NSConditionLock实际使用的性能表现并任何优势,然而条件锁的意义在于对信号量做了面向对象封装;

b、NSLock和NSRecursiveLock在性能表现上与mutex算比较接近,用法上也并无二致,因此,常规情况,NSRecursiveLock和mutex之间的选择,春哥以为更多是习惯和偏好的问题;

c、@synchronized似乎是这些方案当中性能表现最不佳的,那是不是应该完全抛弃呢?春哥倒不这么认为,@synchronized最大的特点在于“快捷”,同步语法仅仅需要一个对象(id指针)作为互斥量,而且还不限于实例对象,类对象也能够支持,这就使得类方法中做同步变得简单不少,block用法也使得代码更紧凑,内存管理更稳健,非常适合一些低频而又不得不同步的逻辑,比如单例初始化、启动加载等等。

综合上述分析与讨论,总结有以下几点原则:

1、总的来看,推荐pthread_mutex作为实际项目的首选方案;

2、对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;

3、如果确认仅有set/get的访问操作,可以选用原子操作属性;

4、对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;

5、条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;

6、@synchronized则适用于低频场景如初始化或者紧急修复使用;

全文摘自(博客起底多线程同步锁(iOS)  作者:SpringOx

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,497评论 0 6
  • 在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? ...
    IAMCJ阅读 3,069评论 2 25
  • 抛砖引玉 说到锁不得不提线程安全,说到线程安全,作为iOS程序员又不得不提 nonatomic 与 atomic ...
    Inlight先森阅读 2,030评论 0 23
  • 本文不介绍各种锁的高级用法,只是整理锁相关的知识点,帮助理解。 锁的作用 防止在多线程(多任务)的情况下对共享资源...
    HelloiWorld阅读 2,874评论 0 8
  • 相爱需要感性,相处却要理性———浅谈电影《男与女》 电影讲述的一对孩子都患有自闭症,陌生的韩国男女在荷兰偶然相识...
    晓曦12阅读 364评论 0 0