多线程的安全隐患
我们用多线程有很多好处,但是也存在安全隐患
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
例子多线程卖票的
- (void)saleTickets
{
self.tickets = 100;
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
for (int i = 0; i < 5; i++)
{
[self sale];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
for (int i = 0; i < 5; i++)
{
[self sale];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
for (int i = 0; i < 5; i++)
{
[self sale];
}
});
}
- (void)sale
{
int oldTickets = self.tickets;
sleep(.2);
oldTickets--;
self.tickets = oldTickets;
NSLog(@"还剩余%i张票", self.tickets);
}
产生的原因
线程同步技术,就是线程访问同一个资源,需要按照特定的次序
解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁
11OSSpinLock01
自旋锁,所有的线程用同一把锁,才能达到加锁的目的等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
需要导入头文件
import
@property (nonatomic, assign) OSSpinLock *lock;
self.lock = OS_SPINLOCK_INIT;
- (void)sale
{
OSSpinLockLock(&_lock);
int oldTickets = self.tickets;
sleep(.2);
oldTickets--;
self.tickets = oldTickets;
NSLog(@"还剩余%i", oldTickets);
OSSpinLockUnlock(&_lock);
}
存钱和取钱也是共用一把锁
IOS10之后就过期了
12OSSpinLock02
让线程阻塞,1让线程睡觉,2忙等状态
尝试加锁
、
14答疑问
用不用加锁 要看是不是多条线程同时访问同一个资源,也要看访问了是干嘛,如果没有修改的话是不需要的
15 os_unfair_lock
用于取代不安全的OSSpinLock ,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import
如果忘记解锁了,线程会一直在那里睡眠,其他的线程拿不到锁,这就是一个死锁。
//初始化锁
self.moneyLock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&_ticketLock);
//加锁
os_unfair_lock_lock(&_ticketLock);
//解锁
os_unfair_lock_unlock(&_ticketLock);
16pthread_mutex01
pthread开头的一半都是通用的
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
需要导入头文#import
//创建一个锁的属性
pthread_mutexattr_t attr;
//初始化属性
pthread_mutexattr_init(&attr);
//设置属性类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//出初始化锁
pthread_mutex_t Lock;
pthread_mutex_init(&lock, &attr);
//加锁
pthread_mutex_lock(&_moneyLock);
//解锁
pthread_mutex_unlock(&_moneyLock);
//销毁相关资源
pthread_mutex_destroy(&_ticketLock);
pthread_mutexattr_destroy(&attr);
//锁的类型
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
17-pthread_mutex02-递归锁
pthread_mutex这种锁我们用完要记得手动销毁
实验一下在这个锁里面再加一个自己的锁,会出现什么情况?
死锁,
或者递归里面有这个锁,都会造成死锁
第一个情况,只需要换一把锁就可以解决
如果是递归,无法换锁,这种情况可以换成递归锁,只需要将这把锁的属性换成PTHREAD_MUTEX_RECURSIVE,他就可以重复加锁。
如果不痛的线程都来访问这个锁的疑问,递归锁是允许同一个线程,对一个锁重复加锁。 如果其他的线程来访问的时候已经加锁了,就不行再加锁,只能等当前线程解锁之后才可以加锁或者访问
//创建一个递归锁
/创建一个锁的属性
pthread_mutexattr_t attr;
//初始化属性
pthread_mutexattr_init(&attr);
//设置属性类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//出初始化锁
pthread_mutex_t Lock;
pthread_mutex_init(&lock, &attr);
8-自旋锁、互斥锁汇编分析
实验,线程在等在锁的时候在做什么事情?
OSSpinLock
让线程sleep时间长一点,然后过掉第一个端点,查看
用lldb-step,他是执行一个oc的指令,可能是几行汇编代码,想要他执行一个汇编代码stepi->si简写,可以看到他一直在执行一个while循环,
pthread_mutex_lock 互斥锁
lldb-c可以快跳到下一个端点
通过上面的方法我们找到底层执行的一个函数mutexwait里面又个syscall,调用了系统级别的函数,执行之后,线程不做事情了,休眠了
os_unfair_lock
他也会执行systcall去休眠,其实他也是互斥锁
low_level lock低级锁,等不到锁就会休眠
19-pthread_mutex03-条件
pthread_mutex在我们创建锁的时候,传进来的type决定什么锁
条件
以前我们加锁的时候很简单,就是等待别的锁放开之后才可以加锁,条件也是另外一条件来放开这把锁.
假如 我们有两条线程同时对一个数组操作,一个添加对象,一个删除对象,我们想哟在删除的时候判断一个只有不是0的时候才可以删除。这样就可以加上一个条件,如果不符合条件,线程就是会休眠,并且放开,刚刚加的锁。如果其他的线程添加了元素可以发送信号来唤醒条件休眠的线程,
使用场景可以用来实现线程等待
20-NSLock、NSRecursiveLock、NSCondition
NSLock就是对mutex普通锁的封装
//初始化锁
self.ticketLock = [[NSLock alloc] init];
self.moneyLock = [[NSLock alloc] init];
//加锁
[self.moneyLock lock];
//解锁
[self.moneyLock unlock];
这种方式更加的面向对象,
可以使用打断点看汇编的方式,查看他底层调用的什么,也可以是gunstep开源的里面查看,对NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
-
NSCondition
NSCondition是对mutex和cond的封装
self.condition= [[NSCondition alloc] init];
_arr = [NSMutableArray array];
//添加对象
//加锁
[self.condition lock];
[self.arr addObject:@"nihao"];
NSLog(@"添加了元素");
//通知condition
[self.condition signal];
//解锁
[self.condition unlock];
//删除对象
//加锁
[self.condition lock];
if (self.arr.count == 0)
{
//进入休眠
[self.condition wait];
}
[self.arr removeLastObject];
NSLog(@"删除了元素");
//解锁
[self.condition unlock];
21答疑
解答删除在添加的后面,多线程不确定谁先执行的时候,也是闲添加再删除,因为又个条件等待
22多线程遗留问题
[condition signal]是不会可以放外边更好,如果两个代码很近中间没有很多的操作,其实都可以,
runloop相关的问题
线程的任务一旦执行完毕,生命周期就结束了,这个线程就无法再使用了,虽然他还在内存中,但是他已经不是激活状态了,还是不能做事情了。可以用手动开启runloop来让线程存活,处于激活状态
为什么不用强指针,而使用runloop
这里说的线程的命,是保证线程的生命周期,保证线程处于激活状态,
dispatch_get_global_queue
//1
[self performSelector:<#(nonnull SEL2)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>];
//3
为什么打印1和3可以,而2不可以,因为1和3不需要runloop的支持的,2的方法本质是讲一个定时器添加到runloop上面,但是子线程的runloop默认是没有开启的,所以不能打印
主线程的runloop默认是打开的,所以主线的UI刷新,点击时间,performSelector,普通的代码平不是交给runloop的
23-NSconditionLock
利用conditionLock可以实现线程之间等待,
//创建一
NSConditionLock *condition;
self.condition= [[NSConditionLock alloc] init];//初始化的时候如果不指定condition的话,他默认就是0
self.condition = [[NSConditionLock alloc] initWithCondition:1];
//加锁
- (void)addPerosn
{
[self.condition lockWhenCondition:2];
NSLog(@"2");
[self.condition unlockWithCondition:3];
}
- (void)deletePerosn
{
[self.condition lockWhenCondition:1];
NSLog(@"1");
[self.condition unlockWithCondition:2];
}
{
[self.condition lockWhenCondition:3];
NSLog(@"3");
[self.condition unlock];
}
24-SerialQueue
直接使用GCD的串行队列,也是可以实现线程同步的
iOS中的线程同步方案
OSSpinLock
os_unfair_lock
pthread_mutex
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
线程同步的本质就是不能让多条线程同时占用一块资源,让他们安顺序来访问,利用串行队列
26semaphore01-最大并发数量
semaphore信号量,
信号量的初始值,可以用来控制线程并发访问的最大数量,比如卖票,希望有三个线程在卖票。
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
初始化
self.semaphore = dispatch_semaphore_create(5);//最大并发数
- (void)saveMoney
{
for (int i = 0; i < 20; i++)
{
//创建20条子线程
[[[NSThread alloc] initWithTarget:self selector:@selector(drawMoney) object:nil] start];
}
}
- (void)drawMoney
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@", [NSThread currentThread]);
dispatch_semaphore_signal(self.semaphore);
}
26-semaphore02-线程同步
dispatch_semaphore_wait(),
做的事情若果信号量的值<=0,当第6个进来的时候,就让当前线程会进入休眠状态,等到信号量>0;执行就-1,然后往下执行后面的代码,
如果信号量>0,开始的时候是5,执行就-1,然后往下执行后面的代码
dispatch_semaphore_signal(),
这个就是让信号量+1;
用信号量保证线程同步,可以设置信号量=1;保证同时只能有一条线程访问
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
27-semaphore03-@synchronized
@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作,传进来的对象一样,找到的就是同一把锁
- (void)saveMoney
{
@synchronized ([self class]) {
[super saveMoney];
}
}
- (void)drawMoney
{
@synchronized ([self class]) {
[super drawMoney];
}
}
同步方案对比
性能从高到低排序
os_unfair_lock ios10才支持
OSSpinLock
dispatch_semaphore ios8支持
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock 对mutex的封装
NSCondition
pthread_mutex(recursive) //递归锁
NSRecursiveLock
NSConditionLock
@synchronized
使用技巧
static dispatch_semaphore_t semaphore ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
semaphore = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//
dispatch_semaphore_signal(semaphore);
29-自旋锁、互斥锁对比
什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器
什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈
automaic
原子不可再分割,原子性操作就说说明这个操作不可以再分割的,他是一个整体,要执行就要全部执行完,其他线程才能执行。
原子性,一旦我们给属性automaic他就会对我们get和set加锁,都是原子性操作,也就是保证线程同步
atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm
它并不能保证使用属性的过程是线程安全的,
reallySetProperty方法里面查看
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}