小码哥iOS学习笔记第二十天: 多线程的安全隐患

1、多线程的安全隐患

  • 资源共享
    • 1块资源 可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 好比多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引起数据错乱和数据安全问题

2、多线程安全隐患示例01 – 存钱取钱

  • 模拟代码以下

  • 运行程序, 结果以下

  • 正常状况, 应该存5000, 取2500, 因此应该剩3500, 可是结果剩了2500
  • 再次运行模拟

  • 能够看到只剩了2000, 这就是多线程的安全隐患问题, 是数据错乱

3、多线程安全隐患示例02 – 卖票

  • 代码模拟以下

  • 运行程序, 模拟卖票

  • 一共卖出10张, 应该剩余0张, 可是结果却剩余3张, 说明数据出现了错乱

4、多线程安全隐患分析和解决方案

一、多线程安全隐患分析

二、多线程安全隐患的解决方案

  • 解决方案:使用线程同步技术(同步,就是协同步调,按预约的前后次序进行)
  • 常见的线程同步技术是:加锁

5、iOS中的线程同步方案

  • iOS中线程加锁有如下几种方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
复制代码

6、准备代码

  • 将上面的多线程安全隐患示例01 – 存钱取钱多线程安全隐患示例02 – 卖票代码封装到一个BaseDemo类中, 具体代码以下图

  • BaseDemo暴露出五个方法, 两个测试调用, 三个线程调用
  • 建立AddLockDemo继承自BaseDemo

  • ViewController中代码以下

7、OSSpinLock(自旋锁)

  • OSSpinLock叫作自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源

一、解决存钱取钱卖票的安全隐患

  • 存钱取钱卖票中加入OSSpinLock

  • 运行程序, 屡次点击屏幕试验, 均可以发现结果正确

二、OSSpinLock目前已经再也不安全,可能会出现优先级反转问题

  • 一个程序中可能会有多个线程, 可是只有一个CPU
  • CPU给线程分配资源, 让他们穿插的执行, 好比有三个线程thread1thread2thread3
  • CPU经过分配, 让thread1执行一段时间后, 接着让thread2执行一段时间, 而后再让thread3执行一段时间
  • 这样就给了咱们有多个线程同时执行任务的错觉
  • 而线程是有优先级的
    • 若是优先级高, CPU会多分配资源, 就会有更多的时间执行
    • 若是优先级低, CPU会减小分配资源, 那么执行的就会慢
  • 那么就可能出现低优先级的线程先加锁,可是CPU更多的执行高优先级线程, 此时就会出现相似死锁的问题
假设经过OSSpinLock给两个线程`thread1`和`thread2`加锁
thread优先级高, thread2优先级低
若是thread2先加锁, 可是尚未解锁, 此时CPU切换到`thread1`
由于`thread1`的优先级高, 因此CPU会更多的给`thread1`分配资源, 这样每次`thread1`中遇到`OSSpinLock`都处于使用状态
此时`thread1`就会不停的检测`OSSpinLock`是否解锁, 就会长时间的占用CPU
这样就会出现相似于死锁的问题
复制代码

8、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

  • 运行程序, 屡次点击屏幕试验, 均可以发现结果正确

9、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
复制代码

一、解决存钱取钱卖票的安全隐患

  • 导入头文件, 建立锁, 加锁解锁

  • 运行程度, 屡次点击屏幕试验, 均可以发现结果正确

二、递归锁

  • 定义PthreadTest类继承自NSObject, 其中recursive是一个递归方法

  • ViewController中代码以下, 点击屏幕后调用PthreadTestrecursive方法

  • 点击屏幕, 能够看到发生了死锁, 这是由于recursive中调用recursive, 此时尚未解锁, 再次进行加锁, 因此发生了死锁

  • 设置pthread初始化时的属性类型为PTHREAD_MUTEX_RECURSIVE, 这样pthread就是一把递归锁

  • 递归锁容许同一线程内, 对同一把锁进行重复加锁, 因此能够看到递归方法调用成功

三、条件

  • PthreadTest中代码以下

  • ViewController中代码以下

  • 当点击屏幕时, 会在array中移除最后一个元素添加一个新元素, 代码中能够看到, 使用不一样线程调用__remove__add两个方法安全

  • 如今的需求是, 只有在array不为空的状况下, 才能执行删除操做, 若是直接运行, 那么可能会先调用__remove在调用__add, 那么就与需求相违背bash

  • 因此, 咱们可使用条件对两个方法进行优化多线程

  • 建立cond函数

  • array.count == 0时, 是程序进入休眠, 只有当array中添加了新数据后在发起信号, 将休眠的线程唤醒

  • 运行程序, 点击屏幕, 能够看到程序先进入__remove方法, 可是却在__add中添加新元素以后再移除元素

10、NSLock、NSRecursiveLock、NSCondition、NSConditionLock

  • NSLockNSRecursiveLockNSConditionNSConditionLock是基于pthread封装的OC对象

一、NSLock

  • AddLockDemo中代码以下, 直接使用NSLock进行加锁

  • ViewController中点击屏幕时调用方法

  • 运行程序, 点击屏幕, 能够看到结果正确

  • 查看GNUStep中关于NSLock的底层代码, 能够看到NSLock是基础pthread封装的normal

二、NSRecursiveLock

  • PthreadTest中代码以下, 使用NSRecursiveLock递归函数加锁解锁

  • ViewController中, 当点击屏幕时调用recursive方法

  • 运行程序, 点击屏幕, 能够看到递归锁的结果

  • 查看GNUStep中关于NSRecursiveLock的底层代码

三、NSCondition

  • PthreadTest中代码以下, 使用NSCondition加锁解锁

  • ViewController中, 当点击屏幕时调用pthreadTest方法

  • 能够看到, 先调用了__remove方法, 可是却在__add中给array添加了新元素以后, 才删除一个元素

  • 查看GNUStep中关于NSCondition的底层代码

四、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设置线程的执行顺序

  • 运行程序, 能够看到打印顺序

11、同步队列解决多线程隐患

  • 使用同步队列, 代码以下图

  • ViewController代码以下

  • 点击屏幕, 能够看到结果正确

12、dispatch_semaphore_t

  • 可使用dispatch_semaphore_t设置信号量为1, 来控制赞成之间只有一条线程能执行, 实际代码以下

  • 运行程序, 点击屏幕, 能够看到打印结果正确

十3、@synchronized

  • @synchronized是对mutex递归锁的封装
  • 源码查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)内部会生成obj对应的递归锁,而后进行加锁、解锁操做

一、解决多线程的安全隐患

  • 使用@synchronized进行加锁

  • 执行代码, 点击屏幕, 效果以下

二、@synchronized底层原理

  • 找到objc_sync_enterobjc_sync_exit两个函数, 分别用于加锁和解锁

  • 查看SyncData

  • 经过所点进去, 找到recursive_mutex_tt

  • 查看recursive_mutex_tt, 能够看到底层是经过os_unfair_recursive_lock封装的锁

  • 接着查看经过对象获取锁的代码

  • 找到LIST_FOR_OBJ, 点击查看

  • 能够看到, 经过传入的对象, 会获取惟一标识所谓锁

十4、iOS线程同步方案性能比较

性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
复制代码

十5、自旋锁、互斥锁比较

  • 什么状况使用自旋锁比较划算?
    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)常常被调用,但竞争状况不多发生
    • CPU资源不紧张
    • 多核处理器
  • 什么状况使用互斥锁比较划算?
    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操做
    • 临界区代码复杂或者循环量大
    • 临界区竞争很是激烈
相关文章
相关标签/搜索