多线程的安全隐患
-
资源共享
- 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
- 比如多个线程访问同一个对象、同一个变量、同一个文件
-
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
多线程安全隐患示例01 – 存钱取钱
- 案例分析
- 1.时间顺序 存钱线程读取金额1000,
- 2.存钱线程还没有存入的时候,取钱线程也读取金额1000
- 3.存钱线程存入1000 存钱线程显示为2000
- 4.取钱金额取出500 显示金额为500
- 5.最后显示为500
多线程安全隐患示例02 – 卖票
- 案例代码
#import "ViewController.h"
@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@property (assign, nonatomic) int money;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self moneyTest];
}
/**
存钱、取钱演示
*/
- (void)moneyTest
{
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self drawMoney];
}
});
}
/**
存钱
*/
- (void)saveMoney
{
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/**
取钱
*/
- (void)drawMoney
{
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/**
卖1张票
*/
- (void)saleTicket
{
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}
/**
卖票演示
*/
- (void)ticketTest
{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
复制代码
多线程安全隐患分析
- 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
- 常见的线程同步技术是:加锁
- 线程A访问Integer的时候先加锁,访问完成后解锁
- 在线程B访问加锁,完成后解锁。
iOS中的线程同步方案
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL) //串行队列
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
@synchronized
OSSpinLock
- OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
- 目前已经不再安全,可能会出现优先级反转问题
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
- 需要导入头文件#import #import <libkern/OSAtomic.h>
- 总结
- 自旋锁:当线程加锁时,如果这个锁已经被其他线程锁上,会一直在等待其开锁。内部做一个while循环占用cpu资源。
- 简单的初始化加锁和解锁,不同的线程必须用的同一把锁才可以。
- 可能会出现优先级反转问题。
- 假设线程1的优先级高于线程2。
- 但是开启任务是先进入线程2,线程2上锁,
- 线程1开启任务了,等待线程2开锁。
- 这个时候由于线程1的优先级高于线程2,要先完成线程1才去做线程2,但是线程1在等待线程2开锁。
- 最终程序死锁。
os_unfair_lock
- os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
- 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
- 需要导入头文件#import <os/lock.h>
- 总结
- 互斥锁代替自旋锁。 用法同上,简单的加锁解锁。
pthread_mutex
- mutex叫做”互斥锁”,等待锁的线程会处于休眠状态,互斥锁也会引起死锁。
- 需要导入头文件#import <pthread.h>
- 总结
- pthread_mutex_init(mutex,NULL) //创建锁
- 可以添加属性,但是在添加属性之前需要初始化属性,属性初始化完要销毁属性
- 最后销毁锁
pthread_mutex – 递归锁
- // 递归锁:允许同一个线程对一把锁进行重复加锁,必须是同一线程。
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
static int count = 0;
if (count < 10) {
count++;
[self otherTest];
}
pthread_mutex_unlock(&_mutex);
}
复制代码
- 总结
- 当调用的函数为递归函数时,还没有解锁就又一次进入方法开始加锁,引起死锁。
- 在创建锁的时候传入 PTHREAD_MUTEX_RECURSIVE
- 递归锁:允许同一个线程对一把锁进行重复加锁,必须是同一线程。
线程1:otherTest(+-) otherTest(+-) otherTest(+-) 线程2:otherTest(等待) 复制代码
- 知道线程1的递归方法全部调用完毕之后,线程2才开始任务。
pthread_mutex – 条件
- // 等待
- pthread_cond_wait(&_cond, &_mutex);
- // 信号
- pthread_cond_signal(&_cond);
- 总结
- 先初始化条件 pthread_cond_init(&_cond, NULL);
- 加入线程1先开启任务,1上锁,线程2等待线程1开锁后再上锁执行。
- 线程1中有判断条件成立后,执行这个pthread_cond_wait(&_cond, &_mutex);
- pthread_cond_wait(&_cond, &_mutex);线程1的锁先解锁,进入休眠状态。
- 线程2发现线程1开锁了就开始执行了,执行完毕后发送信号 pthread_cond_signal(&_cond);
- 线程1接收到信号,等待线程2的开锁,当线程2完成开锁后,线程1上锁,接着执行下面的任务。
NSLock、NSRecursiveLock
- NSLock是对mutex普通锁的封装
- NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
- 总结
- 是上面方法的封装用法方便,不需要很麻烦的创建。
self.ticketLock = [[NSLock alloc] init]; //创建锁 [self.ticketLock lock]; //上锁 [self.ticketLock unlock]; //解锁 复制代码
- NSRecursiveLock 对递归锁的封装。
NSCondition
- NSCondition是对mutex和cond的封装
- 总结
- 对条件锁的封装 不需要初始化条件
@property (strong, nonatomic) NSCondition *condition; self.condition = [[NSCondition alloc] init]; [self.condition lock]; [self.condition wait]; [self.condition signal]; [self.condition unlock]; 复制代码
NSConditionLock
- NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
- (void)__one
{
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
[self.conditionLock unlockWithCondition:2];
}
- (void)__two
{
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
sleep(1);
[self.conditionLock unlockWithCondition:3];
}
- (void)__three
{
[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
[self.conditionLock unlock];
}
复制代码
- 总结
- 初始化条件1,符合条件的加锁
- 2等待条件为2,3等待条件为3.
- 1执行后解锁,设置条件为2
- 条件2的上锁,解锁后设置条件为3
- ...
- 保证了线程的一步一步执行,执行1->2->3
- condition和conditionLock的差别
- condition是1执行到中途解锁休眠等待,2执行后发送通知给1,1被唤醒继续执行
- conditionLock,设置了条件只有条件符合了才能被唤醒执行。
dispatch_semaphore
- semaphore叫做”信号量”
- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(5);
self.ticketSemaphore = dispatch_semaphore_create(1);
self.moneySemaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)__drawMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __drawMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saveMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __saveMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saleTicket
{
dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
[super __saleTicket];
dispatch_semaphore_signal(self.ticketSemaphore);
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
// 线程10、7、6、9、8
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
复制代码
- 总结
- 类似下面的串行队列,并发数设置为1只有一条线程访问资源
- self.semaphore = dispatch_semaphore_create(5); 这个一开始给他5条并发量 线程1、2、3、4、5.
- DISPATCH_TIME_FOREVER 永远
- dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);这个内部会执行命令的同时信号量-1,当信号量<=0值进入等待,等待时间有参数控制。所以这里会开启5条线程做任务,当着5条线程中的任务都还没有完成时,在执行到这里是,就会进入睡眠等待。所以不会开启线程6.
- dispatch_semaphore_signal(self.semaphore); +1,后唤醒了dispatch_semaphore_wait,这个时候线程1完成了任务并被销毁。开启线程6.这是在工作的线程是 线程2、3、4、5、6.依旧是5个线程工作。
dispatch_queue
- 直接使用GCD的串行队列,也是可以实现线程同步的
- 串行队列无法开启新线程,相当于任务只有一个人来做,保证任务的顺序执行
@synchronized
- @synchronized是对mutex递归锁的封装
- 源码查看:objc4中的objc-sync.mm文件
- @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
- 传入一个对象并以这个对象创建一个互斥锁。
- 加入两个方法控制了同一对象就用同一个对象
- 可以创建一个静态变量做个锁保证同一个对象。
- 性能最差并不推荐。
iOS线程同步方案性能比较 仅供参考
- 性能从高到低排序
- os_unfair_lock //底层源码中有看到这个 ios10 开始支持
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive) //递归锁保证递归方法的调用。
- NSRecursiveLock
- NSConditionLock
- @synchronized
自旋锁、互斥锁比较
-
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
-
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈