iOS - 多线程的安全隐患
- 资源共享
 - 1块资源可能会被多个线程共享,也就是
多个线程可能会访问同一块资源 
- 1块资源可能会被多个线程共享,也就是
 - 比如多个线程访问同一个对象、同一个变量、同一个文件
 
- 当多个线程访问同一块资源时,很容易引发
数据错乱和数据安全问题 
- 卖票案例
 
假设总共15张票,使用3个异步线程并发执行,每个线程卖5张票,观察最后的打印结果
1  | @interface ViewController ()  | 
出现票重复卖票,最后没卖完的情况
时序图示意:
分析图:
- 多线程安全隐患的解决方案
 
- 解决方案:使用
线程同步技术(同步,就是协同步调,按预定的先后次序进行) - 常见的线程同步技术是:
加锁 
2.1 iOS中的线程同步方案
- OSSpinLock
 - os_unfair_lock
 - pthread_mutex
 - dispatch_semaphore
 - dispatch_queue(DISPATCH_QUEUE_SERIAL)
 - NSLock
 - NSRecursiveLock
 - NSCondition
 - NSConditionLock
 - @synchronized
 
2.2 同步方案的使用
为了方便调试,我们做个简单封装,将需要加锁的代码放在ZSXBaseDemo中,同时暴漏方法给子类重写:
- (void)__saveMoney;
 
- (void)__drawMoney;
 
- (void)__saleTicket;
 
每一把锁将创建一个ZSXBaseDemo子类,然后重写上述方法,实现各自的加锁方式
ZSXBaseDemo.h
1  | @interface ZSXBaseDemo : NSObject  | 
ZSXBaseDemo.m
1  | @interface ZSXBaseDemo()  | 
2.2.1 OSSpinLock
- OSSpinLock叫做
"自旋锁",等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源 - 目前已经
不再安全,可能会出现优先级反转问题 - 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
 
- 需要导入头文件#import <libkern/OSAtomic.h>
 
2.2.1.1 使用方法:
1  | // 初始化  | 
2.2.1.2 案例
1  | #import "OSSpinLockDemo.h"  | 
对于
卖票,只有__saleTicket方法中需要使用锁,因此也可以使用static关键字创建静态变量,无需声明为属性
1  | - (void)__saleTicket {  | 
2.2.2 os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持- 从底层调用看,等待
os_unfair_lock锁的线程会处于休眠状态,并非忙等 - 需要导入头文件
#import <os/lock.h> 
2.2.2.1 使用方法:
1  | // 初始化  | 
2.2.2.2 案例
1  | #import "OSUnfairLockDemo.h"  | 
2.2.3 pthread_mutex
- mutex叫做
”互斥锁”,等待锁的线程会处于休眠状态 - 需要导入头文件
#import <pthread.h> 
2.2.3.1 使用方法
1  | // 初始化锁  | 
2.2.3.2 案例
1  | @interface MutexDemo ()  | 
2.2.3.3 pthread_mutex-递归锁
递归:允许
同一个线程对一把锁重复加锁
在otherTest方法中,假设该方法中已经加锁,同时会调用另一个也需要加锁的方法
1  | - (void)otherTest {  | 
此时执行代码
代码只会执行到
[self otherTest2];。此时出现死锁,是因为otherTest方法加锁后,调用otherTest2,otherTest2方法开始执行时也会加锁,此时因为otherTest方法还未解锁,otherTest2则进入等待解锁状态,而otherTest需要等待otherTest2方法执行完才继续,所以产生死锁
使用pthread_mutexattr_t配置该锁为递归锁:PTHREAD_MUTEX_RECURSIVE
1  | // 初始化锁  | 
此时可以正常执行完otherTest、otherTest2方法
如果otherTest方法里面是递归调用otherTest自身
1  | - (void)otherTest {  | 
这时候会死循环调用otherTest
若增加一个计数,即可控制递归调用的次数
1  | - (void)otherTest {  | 
执行结果:
2.2.3.4 pthread_mutex-条件锁
业务场景中,可能需要不同线程间的依赖关系,比如线程1需要等待线程2执行完才能继续执行
假设有如下代码:
1  | - (void)otherTest {  | 
使用多线程调用__remove和__add,两者执行顺序不确定。但是希望__remove是在__add之后执行,保证先加、再减。
这时候可以使用条件锁来实现
2.2.3.4.1 使用方法
1  | // 初始化锁  | 
2.2.3.4.2 案例
1  | #import "MutexDemo3.h"  | 
执行结果:
可以保证先添加,再删除
2.2.4 dispatch_semaphore
2.2.5 dispatch_queue(DISPATCH_QUEUE_SERIAL)
2.2.6 NSLock、NSRecursiveLock
2.2.6.1 NSLock是对mutex普通锁的封装
2.2.6.1.1 使用方式
2.2.6.2 NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
2.2.6.3 案例
2.2.6.3.1 NSLock
1  | #import "NSLockDemo.h"  | 
2.2.6.3.2 NSRecursiveLock
1  | #import "NSLockDemo2.h"  | 
执行结果:
2.2.7 NSCondition
- NSCondition是对mutex和cond的封装
 
2.2.7.1 案例
1  | #import "NSConditionDemo.h"  | 
执行结果:
2.2.8 NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
2.2.8.1 案例
1  | #import "NSConditionLockDemo.h"  | 
执行结果:
通过设置Condition,可以实现按想要的顺序执行任务,或者说任务之间的依赖关系
condition默认值是0即:
使用
[[NSConditionLock alloc] init]初始化,condition为 0
使用
- (void)lock代表直接加锁即:
[self.conditionLock lock]
2.2.9 dispatch_queue - 串行队列
- 直接使用
GCD的串行队列,也是可以实现线程同步的 
2.2.9.1 案例
1  | #import "SerialQueueDemo.h"  | 
执行结果:
2.2.10 dispatch_semaphore - 信号量
semaphore叫做”信号量”- 信号量的初始值,可以用来控制线程
并发访问的最大数量 - 信号量的初始值为
1,代表同时只允许1条线程访问资源,保证线程同步 
2.2.10.1 控制最大并发数
- (void)otherTest方法循环创建20个线程执行- (void)test方法,semaphore初始值设置为5
1  | #import "SemaphoreDemo.h"  | 
运行结果:
实现了控制最大并发数5
2.2.10.2 保证线程同步
1  | @interface SemaphoreDemo()  | 
执行结果:
虽然打印结果已经保证线程同步,但是窗口收到了警告
2.2.10.2.1 使用信号量可能会造成线程优先级反转,且无法避免
QoS (Quality of Service),用来指示某任务或者队列的运行优先级;
- 记录了持有者的
api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如dispatch_sync, 如果系统不知道持有者所在的线程,则无法知道应该提高谁的优先级,也就无法解决反转问题。 - 慎用
dispatch_semaphore做线程同步 
dispatch_semaphore容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);
dispatch_semaphore不能避免优先级反转的原因
在调用dispatch_semaphore_wait()的时候,系统不知道哪个线程会调用 dispatch_semaphore_signal()方法,系统无法知道owner信息,无法调整优先级。dispatch_group和semaphore类似,在调用enter()方法的时候,无法预知谁会leave(),所以系统也不知道owner信息
2.2.11 @synchronized
@synchronized是对mutex递归锁的封装- 源码查看:objc4中的objc-sync.mm文件
 @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
2.2.11.1 底层原理
使用哈希表结构,将穿进去的obj作为key,找到低层封装mutex的锁,再进行加锁、解锁操作
@synchronized (obj):
obj如果相同,则代表使用同一把锁
2.2.11.1 案例
1  | #import "SynchronizedDemo.h"  | 
- 拓展
 
3.1 iOS线程同步方案性能比较
性能从高到低排序
- os_unfair_lock
 - OSSpinLock
 - dispatch_semaphore
 - pthread_mutex
 - dispatch_queue(DISPATCH_QUEUE_SERIAL)
 - NSLock
 - NSCondition
 - pthread_mutex(recursive)
 - NSRecursiveLock
 - NSConditionLock
 - @synchronized
 
性能排行仅供参考,不同环境实际效果可能不一样
3.2 自旋锁、互斥锁比较
3.2.1 什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
 - 加锁的代码(临界区)经常被调用,但竞争情况很少发生
 - CPU资源不紧张
 - 多核处理器
 
3.2.2 什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
 - 单核处理器
 - 临界区有IO操作
 - 临界区代码复杂或者循环量大
 - 临界区竞争非常激烈
 
@oubijiexi
本文转载自: 掘金