一、多线程的安全隐患
- 资源共享
- 1块资源 可能会被多个线程共享,也就是多个线程可能会访问同一块资源
- 比如多个线程访问同一个对象、同一个变量、同一个文件
- 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
二、多线程安全隐患示例01 – 存钱取钱
- 模拟代码如下
- 运行程序, 结果如下
- 正常情况, 应该存
5000
, 取2500
, 所以应该剩3500
, 但是结果剩了2500
- 再次运行模拟
- 可以看到只剩了
2000
, 这就是多线程的安全隐患问题, 是数据错乱
三、多线程安全隐患示例02 – 卖票
- 代码模拟如下
- 运行程序, 模拟卖票
- 一共卖出
10
张, 应该剩余0
张, 但是结果却剩余3
张, 说明数据出现了错乱
四、多线程安全隐患分析和解决方案
1、多线程安全隐患分析
2、多线程安全隐患的解决方案
- 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
- 常见的线程同步技术是:加锁
五、iOS中的线程同步方案
- iOS中线程加锁有以下几种方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
复制代码
六、准备代码
- 将上面的
多线程安全隐患示例01 – 存钱取钱
和多线程安全隐患示例02 – 卖票
代码封装到一个BaseDemo
类中, 具体代码如下图
- 在
BaseDemo
暴露出五个方法, 两个测试调用, 三个线程调用 - 创建
AddLockDemo
继承自BaseDemo
-
ViewController
中代码如下
七、OSSpinLock(自旋锁)
-
OSSpinLock
叫做自旋锁
,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
1、解决存钱取钱
和卖票
的安全隐患
- 在
存钱取钱
和卖票
中加入OSSpinLock
- 运行程序, 多次点击屏幕试验, 都可以发现结果正确
2、OSSpinLock
目前已经不再安全,可能会出现优先级反转问题
- 一个程序中可能会有多个线程, 但是只有一个CPU
- CPU给线程分配资源, 让他们穿插的执行, 比如有三个线程
thread1
、thread2
和thread3
- CPU通过分配, 让
thread1
执行一段时间后, 接着让thread2
执行一段时间, 然后再让thread3
执行一段时间 - 这样就给了我们有多个线程同时执行任务的错觉
- 而线程是有优先级的
- 如果优先级高, CPU会多分配资源, 就会有更多的时间执行
- 如果优先级低, CPU会减少分配资源, 那么执行的就会慢
- 那么就可能出现低优先级的线程先加锁,但是CPU更多的执行高优先级线程, 此时就会出现类似死锁的问题
假设通过OSSpinLock给两个线程`thread1`和`thread2`加锁
thread优先级高, thread2优先级低
如果thread2先加锁, 但是还没有解锁, 此时CPU切换到`thread1`
因为`thread1`的优先级高, 所以CPU会更多的给`thread1`分配资源, 这样每次`thread1`中遇到`OSSpinLock`都处于使用状态
此时`thread1`就会不停的检测`OSSpinLock`是否解锁, 就会长时间的占用CPU
这样就会出现类似于死锁的问题
复制代码
八、os_unfair_lock(互斥锁)
-
os_unfair_lock
用于取代不安全的OSSpinLock, 从iOS10
开始才支持 - 从底层调用看, 等待
os_unfair_lock
锁的线程会处于休眠状态, 并非忙等 - 需要导入头文件
#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁, 如果lcok已经被使用, 加锁失败返回false, 如果加锁成功, 返回true
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);
复制代码
解决存钱取钱
和卖票
的安全隐患
- 在存钱取钱和卖票中加入
os_unfair_lock
- 运行程序, 多次点击屏幕试验, 都可以发现结果正确
九、pthread_mutex
-
mutex
叫做互斥锁
,等待锁的线程会处于休眠状态 - 需要导入头文件
#import <pthread.h>
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t pthread;
pthread_mutex_init(&pthread, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 销毁锁
pthread_mutex_destroy(&pthread);
复制代码
- 属性类型的取值
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
复制代码
1、解决存钱取钱
和卖票
的安全隐患
- 导入头文件, 创建锁, 加锁解锁
- 运行程度, 多次点击屏幕试验, 都可以发现结果正确
2、递归锁
- 定义
PthreadTest
类继承自NSObject
, 其中recursive
是一个递归方法
-
ViewController
中代码如下, 点击屏幕后调用PthreadTest
的recursive
方法
- 点击屏幕, 可以看到发生了死锁, 这是因为
recursive
中调用recursive
, 此时还没有解锁, 再次进行加锁, 所以发生了死锁
- 设置
pthread
初始化时的属性类型为PTHREAD_MUTEX_RECURSIVE
, 这样pthread
就是一把递归锁
- 递归锁允许同一线程内, 对同一把锁进行重复加锁, 所以可以看到递归方法调用成功
3、条件
-
PthreadTest
中代码如下
-
ViewController
中代码如下
- 当点击屏幕时, 会在
array
中移除最后一个元素
和添加一个新元素
, 代码中可以看到, 使用不同线程调用__remove
和__add
两个方法 - 现在的需求是, 只有在
array
不为空的情况下, 才能执行删除操作, 如果直接运行, 那么可能会先调用__remove
在调用__add
, 那么就与需求相违背 - 所以, 我们可以使用
条件
对两个方法进行优化 - 创建
cond
- 当
array.count == 0
时, 是程序进入休眠, 只有当array
中添加了新数据后在发起信号, 将休眠的线程唤醒
- 运行程序, 点击屏幕, 可以看到程序先进入
__remove
方法, 但是却在__add
中添加新元素之后再移除元素
十、NSLock、NSRecursiveLock、NSCondition、NSConditionLock
-
NSLock
、NSRecursiveLock
、NSCondition
和NSConditionLock
是基于pthread
封装的OC对象
1、NSLock
-
AddLockDemo
中代码如下, 直接使用NSLock
进行加锁
-
ViewController
中点击屏幕时调用方法
- 运行程序, 点击屏幕, 可以看到结果正确
- 查看
GNUStep
中关于NSLock
的底层代码, 可以看到NSLock
是基础pthread
封装的normal
锁
2、NSRecursiveLock
-
PthreadTest
中代码如下, 使用NSRecursiveLock
对递归函数
加锁解锁
-
ViewController
中, 当点击屏幕时调用recursive
方法
- 运行程序, 点击屏幕, 可以看到
递归锁
的结果
- 查看
GNUStep
中关于NSRecursiveLock
的底层代码
3、NSCondition
-
PthreadTest
中代码如下, 使用NSCondition
加锁解锁
-
ViewController
中, 当点击屏幕时调用pthreadTest
方法
- 可以看到, 先调用了
__remove
方法, 但是却在__add
中给array
添加了新元素之后, 才删除一个元素
- 查看
GNUStep
中关于NSCondition
的底层代码
4、NSConditionLock
-
NSConditionLock
是对NSCondition
的进一步封装
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
// 初始化, 同时设置 condition
- (instancetype)initWithCondition:(NSInteger)condition;
// condition值
@property (readonly) NSInteger condition;
// 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (void)lockWhenCondition:(NSInteger)condition;
// 尝试加锁
- (BOOL)tryLock;
// 尝试加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解锁, 同时设置NSConditionLock实例中的condition值
- (void)unlockWithCondition:(NSInteger)condition;
// 加锁, 如果锁已经使用, 那么一直等到limit为止, 如果过时, 不会加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁, 时间限制到limit, 超时加锁失败
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 锁的name
@property (nullable, copy) NSString *name;
@end
复制代码
- 可以使用
NSConditionLock
设置线程的执行顺序
- 运行程序, 可以看到打印顺序
十一、同步队列解决多线程隐患
- 使用同步队列, 代码如下图
-
ViewController
代码如下
- 点击屏幕, 可以看到结果正确
十二、dispatch_semaphore_t
- 可以使用
dispatch_semaphore_t
设置信号量为1
, 来控制同意之间只有一条线程能执行, 实际代码如下
- 运行程序, 点击屏幕, 可以看到打印结果正确
十三、@synchronized
-
@synchronized
是对mutex
递归锁的封装 - 源码查看:
objc4
中的objc-sync.mm
文件 -
@synchronized(obj)
内部会生成obj
对应的递归锁,然后进行加锁、解锁操作
1、解决多线程的安全隐患
- 使用
@synchronized
进行加锁
- 执行代码, 点击屏幕, 效果如下
2、@synchronized底层原理
- 找到
objc_sync_enter
和objc_sync_exit
两个函数, 分别用于加锁和解锁
- 查看
SyncData
- 通过所点进去, 找到
recursive_mutex_tt
- 查看
recursive_mutex_tt
, 可以看到底层是通过os_unfair_recursive_lock
封装的锁
- 接着查看通过
对象
获取锁的代码
- 找到
LIST_FOR_OBJ
, 点击查看
- 可以看到, 通过传入的
对象
, 会获取唯一标识所谓锁
十四、iOS线程同步方案性能比较
性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
复制代码
十五、自旋锁、互斥锁比较
- 什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
- 什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈