iOS多线程demogit
iOS多线程之--NSThreadgithub
iOS多线程相关面试题bash
若是一段代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。若是每次运行结果和单线程运行的结果是同样的,并且其余的变量的值也和预期的是同样的,就是线程安全
的。通常来讲当多个线程访问同一块资源
(同一个对象、同一个变量、同一个文件)时,很容易引起数据错乱和数据安全问题。网络
好比说有个售票系统,开2个线程(至关于2个售票窗口)同时进行售票,售票过程是这样的:首先从数据库中取出余票数量,若是余票数量大于0,那么就进行售票,售1张票耗时1秒钟,出票成功后再将票数减一而后存进数据库。那2个线程同时售票会有什么问题呢?假设余票数量是10,咱们如今模拟一下售票过程:多线程
从上面已经能够看出问题了,2个线程总共卖出了2张票,结果数据库中还剩余9张票,与咱们的预期不符,因此这不是线程安全的。下面咱们来看实际代码演示:并发
- (void)noLock{
self.ticketCount = 10;
// 线程1(窗口1)
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
for (NSInteger i = 0; i < 5; i++) {
[self noLockSaleTicket];
}
}];
thread1.name = @"窗口1";
[thread1 start];
// 线程2(窗口2)
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
for (NSInteger i = 0; i < 5; i++) {
[self noLockSaleTicket];
}
}];
thread2.name = @"窗口2";
[thread2 start];
}
// 不加锁时售票过程
- (void)noLockSaleTicket{
NSInteger oldCount = self.ticketCount; // 取出余票数量
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f]; // 模拟售票耗时1秒
self.ticketCount = --oldCount; // 售出一张票后更新剩余票数
}
NSLog(@"剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
}
****************打印结果****************
2020-01-01 10:31:11.260165+0800 MultithreadingDemo[51984:5695718] 剩余票数:9--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:11.260165+0800 MultithreadingDemo[51984:5695717] 剩余票数:9--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:12.263126+0800 MultithreadingDemo[51984:5695718] 剩余票数:8--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:12.263152+0800 MultithreadingDemo[51984:5695717] 剩余票数:8--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:13.263691+0800 MultithreadingDemo[51984:5695717] 剩余票数:7--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:13.263693+0800 MultithreadingDemo[51984:5695718] 剩余票数:7--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:14.264340+0800 MultithreadingDemo[51984:5695717] 剩余票数:6--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:14.264340+0800 MultithreadingDemo[51984:5695718] 剩余票数:6--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
2020-01-01 10:31:15.265322+0800 MultithreadingDemo[51984:5695717] 剩余票数:5--<NSThread: 0x60000373b880>{number = 8, name = 窗口1}
2020-01-01 10:31:15.265322+0800 MultithreadingDemo[51984:5695718] 剩余票数:5--<NSThread: 0x60000373b600>{number = 9, name = 窗口2}
复制代码
咱们是采用线程同步
技术来解决多线程的安全隐患问题,所谓同步,就是协同步调,按预约的前后次序进行。常见的线程同步技术就是加锁
。从上面案例能够看出,致使出现问题的缘由就是2个线程同时在对数据库进行读写操做,加锁就是在有线程正在进行读写操做时将读写操做的代码锁住,这样其余线程就不能在这个时候进行读写操做,等前面的线程完成操做后再解锁,而后后面的线程才能进行读写操做。换句话说加锁就是让某段代码在同一时刻只能有一个线程在执行。异步
在iOS开发中,有多种线程同步方案来确保线程安全,下面来一一介绍:
所谓自旋锁
,就是等待锁的线程是处于忙等
状态。所谓忙等
,咱们能够理解为就是一个while循环
,只要发现锁是加锁状态就一直循环,直到发现锁处于未加锁状态才中止循环。因此自旋锁一直占用着CPU资源,不过自旋锁的效率是很是高的,一旦锁被释放,等待锁的线程立马就能够感知到。通常来讲,若是被加锁的代码块所需的执行时间很是短就可使用自旋锁,可是若是是很是耗时的话就不建议使用自旋锁了,由于等待锁的线程一直忙等很是耗CPU资源。
自旋锁的实现原理比较简单,咱们能够定义一个BOOL类型变量isLock用来表示锁的状态,其初始化为NO表示未加锁。若是要加锁的代码块用A表示,当线程1要执行A以前先判断锁的状态是未加锁状态,因此线程1获取到锁并将isLock置为YES来加锁,而后开始执行A代码块,执行结束后将isLock置为NO来解锁。若是线程1正在执行代码块A时线程2也想要执行代码块A,结果发现如今是加锁状态,因此线程2开始while循环进行忙等,直到线程1解锁线程2才结束while循环而得到锁。忙等、加锁和解锁能够用以下的伪代码来表示:
// while循环进行忙等,循环体里面什么都不用作,只要是加锁状态就一直循环
while(_isLock){
}
// 执行代码块A以前加锁
_isLock = YES;
// 执行代码块A
……
// 执行完代码块A后解锁
_isLock = NO;
复制代码
iOS中的OSSpinLock
就是自旋锁,这是一个C语言
实现的锁,使用时须要导入头文件#import <libkern/OSAtomic.h>
。仍是用前面的售票的例子来进行演示,因为致使线程安全问题的就是售票过程的代码块,因此咱们只需对这一部分代码进行加锁解锁操做。
- (void)OSSpinLockSaleTicket{
if (!_spinLock) { // 初始化锁
_spinLock = OS_SPINLOCK_INIT;
}
// 加锁
OSSpinLockLock(&_spinLock);
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_spinLock);
}
****************打印结果****************
2020-01-01 17:23:37.832503+0800 MultithreadingDemo[64676:6658463] 剩余票数:9--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:38.837188+0800 MultithreadingDemo[64676:6658463] 剩余票数:8--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:39.843327+0800 MultithreadingDemo[64676:6658463] 剩余票数:7--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:40.848290+0800 MultithreadingDemo[64676:6658463] 剩余票数:6--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:41.850766+0800 MultithreadingDemo[64676:6658463] 剩余票数:5--<NSThread: 0x600003a54b40>{number = 7, name = 窗口1}
2020-01-01 17:23:42.868658+0800 MultithreadingDemo[64676:6658464] 剩余票数:4--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:43.872599+0800 MultithreadingDemo[64676:6658464] 剩余票数:3--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:44.878190+0800 MultithreadingDemo[64676:6658464] 剩余票数:2--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:45.880620+0800 MultithreadingDemo[64676:6658464] 剩余票数:1--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
2020-01-01 17:23:46.885194+0800 MultithreadingDemo[64676:6658464] 剩余票数:0--<NSThread: 0x600003a54c80>{number = 8, name = 窗口2}
复制代码
能够看到加锁后运行结果就和咱们预期的同样了。不过这种加锁方式从iOS10开始就已经被弃用了
,由于这种加锁方式可能会致使出现优先级反转
的问题。所谓优先级反转,就是可能会出现高优先级任务所需的资源被低优先级任务给锁住了而致使高优先级任务被阻塞,从而高优先级任务迟迟得不到调度。但其余中等优先级的任务却能抢到CPU资源。从现象上来看,好像是中优先级的任务比高优先级任务具备更高的优先权。
从iOS10开始苹果再也不建议使用OSSpinLock
进行加锁,取而代之的是用os_unfair_lock
进行加锁。其也是用C语言实现的,使用时须要导入头文件#import <os/lock.h>
。使用方法以下:
- (void)osUnfairLockSaleTicket{
// 初始化锁(使用dispatch_once只是为了保证锁只被初始化一次)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_unfairLock = OS_UNFAIR_LOCK_INIT;
});
// 加锁
os_unfair_lock_lock(&_unfairLock);
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"os_unfair_lock剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
// 解锁
os_unfair_lock_unlock(&_unfairLock);
}
复制代码
和自旋锁不一样的是,等待互斥锁
的线程在等待过程当中是处于休眠状态
。一旦锁被其余线程释放,处于休眠状态的线程就会被唤醒。因此线程在等待互斥锁的过程当中是不占用CPU资源的,可是唤醒线程是须要消耗必定时间的,因此互斥锁的效率要比自旋锁低。
pthread_mutex
就是一种互斥锁,它是跨平台的。使用时须要导入头文件#import <pthread.h>
。pthread_mutex
使用起来要比前面介绍的那些锁要麻烦,在初始化锁以前首先要定义一个锁的属性,而后根据锁的属性来初始化锁(初始化锁时属性参数能够直接传NULL进去,传NULL就是使用默认类型)。由于pthread_mutex
能够设置几种不一样类型的锁,设置属性时要指定锁的类型,锁的类型包括如下几种:
// pthread_mutex互斥锁属性的类型
#define PTHREAD_MUTEX_NORMAL 0 // 常规的锁(默认类型)
#define PTHREAD_MUTEX_ERRORCHECK 1 // 检查错误类型的锁(通常用不上)
#define PTHREAD_MUTEX_RECURSIVE 2 // 递归类型的锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL // 默认类型为常规类型
复制代码
另外,当再也不须要使用锁时记得将锁和锁的属性释放(前面介绍的两种锁是没有提供销毁锁的API的),释放方法以下:
// 释放锁的属性
pthread_mutexattr_destroy(&attr);
// 释放锁
pthread_mutex_destroy(&mutexLock);
复制代码
像前面介绍的2种锁都是常规的锁,下面咱们来看下pthread_mutex
的常规锁要如何使用。
- (void)pthreadMutexLockSaleTicket{
// 保证锁只被初始化一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 初始化锁的属性
pthread_mutexattr_t attr; // 建立锁的属性
pthread_mutexattr_init(&attr); // 初始化锁的属性
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 设置锁属性的类型
// 根据锁的属性来初始化锁(属性参数传NULL也是一个常规锁)
pthread_mutex_init(&_pthreadMutexLock, &attr);
});
// 加锁
pthread_mutex_lock(&_pthreadMutexLock);
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"pthread_mutex剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
// 解锁
pthread_mutex_unlock(&_pthreadMutexLock);
}
复制代码
pthread_mutex递归锁
和pthread_mutex常规锁
在使用方法上是同样的,只需把属性的类型参数由PTHREAD_MUTEX_NORMAL
改成PTHREAD_MUTEX_RECURSIVE
就能够了。
可是到底什么是递归锁
呢?什么状况下须要用到递归锁呢?提及递归,咱们首先想到的就是函数的递归调用,也就是在函数内部又调用了本身。递归锁
主要就是用在函数的递归调用场合的,其特色就是锁里面又加锁,换句话说就是同一把锁能够重复加屡次。若是在这种场合下咱们用常规锁会出现什么问题呢?咱们看下下面这个例子:
- (void)test{
// 加锁
pthread_mutex_lock(&_pthreadMutexNotmalLock);
// 加锁代码为递归调用
static NSInteger i = 5;
NSInteger temp = i--;
if (temp > 0) {
[self test];
}
NSLog(@"%ld",temp);
// 解锁
pthread_mutex_unlock(&_pthreadMutexNotmalLock);
}
复制代码
若是某个线程调用上面这个方法的话就会死锁,不会有任何打印信息。由于这是一个常规锁,当线程第一次调用test方法时,这个线程获取到锁,此时tem>0,会第二次调用test(注意这个时候锁没有被释放),因此第二次调用test时发现锁此时是加锁状态,只能在这里等锁释放后才能继续日后执行。而第一次调用test又必须等第二次调用test结束了才能继续往下执行来释放锁,这就形成了死锁。
咱们再来看看换成递归锁会怎么样:
- (void)pthreadMutexRecursiveLockTest{
// 保证锁只被初始化一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 初始化锁的属性
pthread_mutexattr_t attr; // 建立锁的属性
pthread_mutexattr_init(&attr); // 初始化锁的属性
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 设置锁属性的类型为常规锁的话就会形成死锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 设置锁属性的类型为递归锁
// 根据锁的属性来初始化锁
pthread_mutex_init(&_pthreadMutexRecursiveLock, &attr);
});
// 加锁
pthread_mutex_lock(&_pthreadMutexRecursiveLock);
// 加锁代码为递归调用
static NSInteger i = 5;
NSInteger temp = i--;
if (temp > 0) {
[self pthreadMutexRecursiveLockTest];
}
NSLog(@"pthread_mutex递归锁---%ld",temp);
// 解锁
pthread_mutex_unlock(&_pthreadMutexRecursiveLock);
}
****************打印结果****************
2020-01-02 10:28:09.961983+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---0
2020-01-02 10:28:09.962160+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---1
2020-01-02 10:28:09.962310+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---2
2020-01-02 10:28:09.962407+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---3
2020-01-02 10:28:09.962471+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---4
2020-01-02 10:28:09.962526+0800 MultithreadingDemo[59365:5508927] pthread_mutex递归锁---5
复制代码
可见换成递归锁后打印结果和咱们预期同样。那递归锁为何能够重复加锁而不会形成死锁呢?我这里提供一个本身实现递归锁的思路:前面介绍自旋锁时,咱们能够用一个BOOL类型的变量来记录锁加锁、解锁状态,在递归锁里面咱们还用BOOL类型的变量的话显然就行不通了。咱们能够用用一个int类型的变量lockCount(加锁次数)来记录锁的状态,每加一次锁lockCount就+1,每次解锁lockCount就-1,当lockCount为0时就表示是未加锁的状态。固然,咱们还要保证只有同一个线程才能重复加锁,而其余线程来访问的话仍是须要等待锁的释放,因此咱们还必须将当前持有锁的线程给记录下来。
当某线程获取了锁对象,但由于某些条件没有知足,须要在这个条件上等待,直到条件知足才可以往下继续执行时,就须要用到条件锁
。
举个例子,游戏中有产生敌人和杀死敌人2个方法是用同一把锁进行加锁的,杀死敌人有个前提条件就是必须有敌人存,若是在杀死敌人的线程获取到锁后发现敌人不存在,那这个线程就要等待,等有新的敌人产生了再进行杀死操做。好比下面代码中的案例,程序先调用了killEnemy
方法,后调用createEnemy
方法,此时就会出现条件等待。(实际开发中可能并不会这样设计代码逻辑,这里只是为了讲解条件锁的运行流程才这样设计。)
- (void)pthreadMutexConditionLockTest{
[self initPthreadConditionLock]; // 初始化锁、条件和一些相关数据
// 建立一个线程调用killEnemy方法(此时尚未敌人,因此会进入等待状态)
dispatch_queue_t queue = dispatch_queue_create("lock", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"killEnemy开始--%@<##>",[NSThread currentThread]);
[self killEnemy];
NSLog(@"killEnemy结束--%@<##>",[NSThread currentThread]);
});
// 2秒后再建立一个线程调用createEnemy方法来产生敌人
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), queue, ^{
NSLog(@"createEnemy开始--%@<##>",[NSThread currentThread]);
[self createEnemy];
NSLog(@"createEnemy结束--%@<##>",[NSThread currentThread]);
});
}
// 初始化锁、条件和一些相关数据(确保只初始化一次)
- (void)initPthreadConditionLock{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_mutex_init(&_pthreadMutexConditionLock, NULL); // 第二个参数传NULL时是一个常规锁
pthread_cond_init(&_pthreadCondition, NULL); // 初始化条件,第二个参数通常就传NULL
});
if (!_enemyArr) {
_enemyArr = [NSMutableArray array];
}else{
[_enemyArr removeAllObjects];
}
}
// 杀死敌人
- (void)killEnemy{
// 加锁
pthread_mutex_lock(&_pthreadMutexConditionLock);
if (_enemyArr.count == 0) {
NSLog(@"尚未敌人,进入等待状态");
pthread_cond_wait(&_pthreadCondition, &_pthreadMutexConditionLock); // 等待将锁和条件传进去
}
[_enemyArr removeLastObject];
NSLog(@"杀死了敌人");
// 解锁
pthread_mutex_unlock(&_pthreadMutexConditionLock);
}
// 产生敌人
- (void)createEnemy{
// 加锁
pthread_mutex_lock(&_pthreadMutexConditionLock);
NSObject *enemyObj = [NSObject new];
[_enemyArr addObject:enemyObj];
// 发送信号唤醒一条等待条件的线程(发送信号的代码放在解锁代码的前面和后面均可以,放在前面的话就是先发信号唤醒等待的线程,等解锁后等待的线程才能得到锁;放在后面的话当前线程就会先解锁,而后发送信号唤醒等待的线程,等待的线程立马就能够得到锁。)
pthread_cond_signal(&_pthreadCondition);
// pthread_cond_broadcast(&_pthreadCondition); // 发送广播唤醒全部等待条件的线程
if(_enemyArr.count == 1) NSLog(@"敌人数从0变为1,唤醒等待中的线程。");
// 解锁
pthread_mutex_unlock(&_pthreadMutexConditionLock);
}
****************打印结果****************
2020-01-02 12:10:21.386852+0800 MultithreadingDemo[59656:5546517] killEnemy开始--<NSThread: 0x60000026ac00>{number = 3, name = (null)}<##>
2020-01-02 12:10:21.386984+0800 MultithreadingDemo[59656:5546517] 尚未敌人,进入等待状态
2020-01-02 12:10:23.386917+0800 MultithreadingDemo[59656:5546516] createEnemy开始--<NSThread: 0x6000002eb240>{number = 6, name = (null)}<##>
2020-01-02 12:10:23.387074+0800 MultithreadingDemo[59656:5546516] 敌人数从0变为1,唤醒等待中的线程。
2020-01-02 12:10:23.387217+0800 MultithreadingDemo[59656:5546516] createEnemy结束--<NSThread: 0x6000002eb240>{number = 6, name = (null)}<##>
2020-01-02 12:10:23.387219+0800 MultithreadingDemo[59656:5546517] 杀死了敌人
2020-01-02 12:10:23.387329+0800 MultithreadingDemo[59656:5546517] killEnemy结束--<NSThread: 0x60000026ac00>{number = 3, name = (null)}<##>
复制代码
下面咱们来说解一下上面代码的运行流程(关于一些初始化操做就不在这里说了),咱们把调用killEnemy
方法的线程叫killThread
,把调用createEnemy
的线程叫createThread
线程。
killEnemy
方法,killThread
线程获取到锁。killThread
释放锁并进入等待状态。createEnemy
方法,createThread
发现锁是未加锁状态,因此获取到了锁并执行生成敌人的操做。createThread
线程会释放锁)。killThread
线程收到条件信号后被唤醒并从新获取到锁。killThread
线程被唤醒后开始执行条件等待以后的代码,等杀死敌人后将锁释放。那么实际开发中什么场合下会用到条件锁呢?
咱们能够用条件锁来创建线程中的依赖关系。好比用2个线程去执行用户登陆和获取用户信息这两个网络请求(用户登陆成功后会返回一个token,须要用这个token去获取用户信息,也就是说获取用户信息是依赖于用户登陆的),这两个请求的执行前后顺序是不肯定的,因此就有可能出现执行请求用户信息操做时用户都尚未登陆,那么这个时候就可使用条件锁进行等待,等登陆成功返回token后再发送信号将其唤醒。
前面介绍的锁都是C语言实现的,在iOS开发中使用的并非不少,咱们更多使用的是OC封装的锁。NSLock
就是OC对pthread_mutex
常规锁的封装。使用起来比较简单,以下所示:
- (void)nsLockSaleTicket{
// 初始化锁
if (!_nsLock) {
_nsLock = [[NSLock alloc] init];
}
// 加锁
[_nsLock lock];
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"NSLock剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
// 解锁
[_nsLock unlock];
}
复制代码
NSLock
还有下面两个方法:
/*
尝试加锁
若是获取到了锁就加锁并返回YES
若是如今锁被另一个线程持有,那就返回NO,并且不会在这里等待锁的释放,而是会继续执行后面的代码。
*/
- (BOOL)tryLock;
/*
在某个时间以前尝试加锁
若是在这个时间以前获取到了锁就加锁并返回YES
若是到这个时间点还没获取到锁就返回NO,并且不会在这里等待锁的释放,而是会继续执行后面的代码。
*/
- (BOOL)lockBeforeDate:(NSDate *)limit;
复制代码
NSRecursiveLock
是一个递归锁
,是OC对pthread_mutex递归锁
的封装。NSRecursiveLock
的API调用方法和NSLock
是同样的。
- (void)nsRecursiveLockTest{
if (!_nsRecursiveLock) { // 初始化锁
_nsRecursiveLock = [[NSRecursiveLock alloc] init];
}
// 加锁
[_nsRecursiveLock lock];
// 加锁代码为递归调用
static NSInteger i = 5;
NSInteger temp = i--;
if (temp > 0) {
[self nsRecursiveLockTest];
}
NSLog(@"NSRecursiveLock 递归锁---%ld",temp);
// 解锁
[_nsRecursiveLock unlock];
}
复制代码
NSCondition
是一个条件锁
,是OC对pthread_mutex条件锁
的封装。NSCondition
的API和使用方式和pthread_mutex条件锁
是同样的,将前面条件锁的案例换成NSCondition
,代码以下:
- (void)nsConditionTest{
// 初始化锁
if (!_nsCondition) {
_nsCondition = [[NSCondition alloc] init];
_enemyArr = [NSMutableArray array];
}
// 建立一个线程调用killEnemy1方法(此时尚未敌人,因此会进入等待状态)
dispatch_queue_t queue = dispatch_queue_create("NSCondition", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"killEnemy1开始--%@<##>",[NSThread currentThread]);
[self killEnemy1];
NSLog(@"killEnemy1结束--%@<##>",[NSThread currentThread]);
});
// 2秒后再建立一个线程调用createEnemy1方法来产生敌人
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), queue, ^{
NSLog(@"createEnemy1开始--%@<##>",[NSThread currentThread]);
[self createEnemy1];
NSLog(@"createEnemy1结束--%@<##>",[NSThread currentThread]);
});
}
// 杀死敌人
- (void)killEnemy1{
// 加锁
[_nsCondition lock];
if (_enemyArr.count == 0) {
NSLog(@"尚未敌人,进入等待状态");
[_nsCondition wait]; // 等待
}
[_enemyArr removeLastObject];
NSLog(@"杀死了敌人");
// 解锁
[_nsCondition unlock];
}
// 产生敌人
- (void)createEnemy1{
// 加锁
[_nsCondition lock];
NSObject *enemyObj = [NSObject new];
[_enemyArr addObject:enemyObj];
// 发送信号唤醒一条等待条件的线程
[_nsCondition signal];
// [_nsCondition broadcast]; // 发送广播唤醒全部等待条件的线程
if(_enemyArr.count == 1) NSLog(@"敌人数从0变为1,唤醒等待中的线程。");
// 解锁
[_nsCondition unlock];
}
复制代码
NSConditionLock
也是一个条件锁,它是对NSCondition
的进一步封装,与NSCondition
不一样的是NSConditionLock
能够设置条件值。咱们能够经过设置不一样的条件值来创建不一样线程之间的依赖关系。好比下面案例中获取用户信息
操做要依赖用户登录
操做,其加锁流程以下:
[_nsConditionLock lockWhenCondition:2]
,这句代码的意思是当条件值是2时就获取锁进行加锁,因为初始化的条件值是1,因此加锁失败,该线程进入等待状态,等条件值变为2时就会被唤醒。[_nsConditionLock lockWhenCondition:1]
,而此时条件值正好是1,因此加锁成功,开始登录,登录成功后执行[_nsConditionLock unlockWithCondition:2]
,意思是当前线程解锁,并将条件设置为2。- (void)nsConditionLockTest{
// 用条件值初始化锁(也能够直接[[NSConditionLock alloc] init]来初始化,这样初始化的条件值是0)
if(!_nsConditionLock){
_nsConditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
// 开启一个线程执行获取用户信息操做
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"开始获取用户信息");
[self getUserInfoTest];
NSLog(@"获取用户信息结束");
});
// 1秒钟后开启另外一个线程执行登录操做
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), queue, ^{
NSLog(@"开始登录");
[self loginTest];
NSLog(@"结束登录");
});
}
// 模拟登录
- (void)loginTest{
[_nsConditionLock lockWhenCondition:1];
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"登录成功");
[_nsConditionLock unlockWithCondition:2];
}
// 模拟获取用户信息
- (void)getUserInfoTest{
[_nsConditionLock lockWhenCondition:2];
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"获取用户信息成功");
[_nsConditionLock unlock];
}
****************打印结果****************
2020-01-02 22:04:39.682835+0800 MultithreadingDemo[90902:8539476] 开始获取用户信息
2020-01-02 22:04:40.682996+0800 MultithreadingDemo[90902:8539474] 开始登录
2020-01-02 22:04:41.684167+0800 MultithreadingDemo[90902:8539474] 登录成功
2020-01-02 22:04:41.684664+0800 MultithreadingDemo[90902:8539474] 结束登录
2020-01-02 22:04:42.689556+0800 MultithreadingDemo[90902:8539476] 获取用户信息成功
2020-01-02 22:04:42.689911+0800 MultithreadingDemo[90902:8539476] 获取用户信息结束
复制代码
上面咱们都是经过[_nsConditionLock lockWhenCondition:1]
来加锁,这种方式要等到条件值知足要求时才能加锁成功。咱们也能够直接经过[_nsConditionLock lock]
这种方式来加锁,这种方式无论条件值是多少均可以加锁成功。
线程同步的本质就是保证同一时间只有一个线程访问要加锁的代码块,那么咱们能够将要加锁的代码块当作成一个任务同步添加到GCD的串行队列中,串行队列能够保证一次只有一个任务在执行,前一个任务执行完了才能执行下一个任务。代码以下:
- (void)serialQueueSaleTicket{
if (!_serialQueue) {
_serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
}
dispatch_sync(_serialQueue, ^{ // 要加锁的代码块当成任务同步添加到队列中
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"串行队列--剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
});
}
****************打印结果****************
2020-01-02 23:14:34.094617+0800 MultithreadingDemo[1146:25021] 串行队列--剩余票数:9--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:35.096506+0800 MultithreadingDemo[1146:25022] 串行队列--剩余票数:8--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:36.099239+0800 MultithreadingDemo[1146:25021] 串行队列--剩余票数:7--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:37.099940+0800 MultithreadingDemo[1146:25022] 串行队列--剩余票数:6--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:38.103534+0800 MultithreadingDemo[1146:25021] 串行队列--剩余票数:5--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:39.105606+0800 MultithreadingDemo[1146:25022] 串行队列--剩余票数:4--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:40.107929+0800 MultithreadingDemo[1146:25021] 串行队列--剩余票数:3--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:41.111818+0800 MultithreadingDemo[1146:25022] 串行队列--剩余票数:2--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
2020-01-02 23:14:42.117835+0800 MultithreadingDemo[1146:25021] 串行队列--剩余票数:1--<NSThread: 0x6000021f5700>{number = 6, name = 窗口1}
2020-01-02 23:14:43.119191+0800 MultithreadingDemo[1146:25022] 串行队列--剩余票数:0--<NSThread: 0x6000021f5800>{number = 7, name = 窗口2}
复制代码
dispatch_semaphore_t
是GCD中用于控制最大并发数的,其初始化时设置的信号量值就是最大并发数,当信号量值初始化为1时表示最大并发数为1,也就达到了同一时间只有一个线程访问要加锁代码块的目的,从而实现代码同步。
// dispatch_semaphore_wait()和dispatch_semaphore_signal()之间的代码就是要加锁的代码
- (void)dispatchSemaphoreSaleTicket{
if (!_semaphore) { // 初始化信号量为1,表最大并发数为1
_semaphore = dispatch_semaphore_create(1);
}
// 若是信号量>0,则信号量-1并执行后面代码
// 若是信号量<=0,则线程进入等待状态(线程休眠),第二个参数是等待时间,等信号量大于0或者等待超时时开始执行后续代码(DISPATCH_TIME_FOREVER表示永不超时)
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSInteger oldCount = self.ticketCount;
if (oldCount > 0) {
[NSThread sleepForTimeInterval:1.0f];
self.ticketCount = --oldCount;
}
NSLog(@"信号量--剩余票数:%ld--%@",self.ticketCount,[NSThread currentThread]);
// 执行dispatch_semaphore_signal()函数信号量+1
dispatch_semaphore_signal(_semaphore);
}
****************打印结果****************
2020-01-02 23:33:14.201100+0800 MultithreadingDemo[1454:34572] 信号量--剩余票数:9--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:15.202743+0800 MultithreadingDemo[1454:34573] 信号量--剩余票数:8--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:16.208335+0800 MultithreadingDemo[1454:34572] 信号量--剩余票数:7--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:17.213433+0800 MultithreadingDemo[1454:34573] 信号量--剩余票数:6--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:18.218910+0800 MultithreadingDemo[1454:34572] 信号量--剩余票数:5--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:19.224498+0800 MultithreadingDemo[1454:34573] 信号量--剩余票数:4--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:20.230172+0800 MultithreadingDemo[1454:34572] 信号量--剩余票数:3--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:21.235774+0800 MultithreadingDemo[1454:34573] 信号量--剩余票数:2--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
2020-01-02 23:33:22.237456+0800 MultithreadingDemo[1454:34572] 信号量--剩余票数:1--<NSThread: 0x600002064500>{number = 7, name = 窗口1}
2020-01-02 23:33:23.238408+0800 MultithreadingDemo[1454:34573] 信号量--剩余票数:0--<NSThread: 0x600002064600>{number = 8, name = 窗口2}
复制代码
@synchronized
是使用起来最简单的锁,写法很简单:@synchronized (obj) {}
。它的底层是对pthread_mutex递归锁
的封装,其内部会根据小括号传入的对象生成对应的递归锁,而后进行加锁解锁操做。不过这个锁虽然写起来方便,可是性能比较差。
- (void)synchronizedTest{
@synchronized (self) {
// 加锁代码为递归调用
static NSInteger i = 5;
NSInteger temp = i--;
if (temp > 0) {
[self nsRecursiveLockTest];
}
NSLog(@"NSRecursiveLock 递归锁---%ld",temp);
}
}
****************打印结果****************
2020-01-02 23:55:14.626155+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---0
2020-01-02 23:55:14.626455+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---1
2020-01-02 23:55:14.626658+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---2
2020-01-02 23:55:14.626836+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---3
2020-01-02 23:55:14.627004+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---4
2020-01-02 23:55:14.627160+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---5
2020-01-02 23:55:14.627601+0800 MultithreadingDemo[1717:43609] NSRecursiveLock 递归锁---5
复制代码
iOS线程同步方案性能比较:
性能从高到低排序:
什么状况使用自旋锁比较划算?
什么状况使用互斥锁比较划算?
当多个线程同时访问一个资源时容易形成数据错乱,其根本缘由就在于写的操做上,由于在没有进行写操做时,同一个数据不管同时读多少次获得的结果都是同样的。并且读操做和写操做是不能同时进行的。因此为了保证读写安全,必须知足如下3个条件:
上面的场景就是典型的“多读单写”,常常用于文件等数据的读写操做,iOS中的实现方案有pthread_rwlock
(读写锁)和GCD的dispatch_barrier_async
(异步栅栏函数)。
pthread_rwlock
(读写锁)pthread_rwlock
须要引入头文件#import <pthread.h>
,等待锁的线程会进入休眠。下面是相关的API:
// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读数据时加锁
pthread_rwlock_rdlock(&lock);
// 读数据时尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写数据时加锁
pthread_rwlock_wrlock(&lock);
// 写数据时尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁锁
pthread_rwlock_destroy(&lock);
复制代码
下面咱们来看下实际代码演示:
- (void)pthreadRwlockTest{
// 初始化锁
pthread_rwlock_init(&_pthreadRwlock, NULL);
dispatch_queue_t queue = dispatch_queue_create("rwlock", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 2; i++) {
dispatch_async(queue, ^{
[self read];
[self read];
[self write];
[self write];
[self read];
[self read];
});
}
}
// 读操做
- (void)read{
// 读加锁
pthread_rwlock_rdlock(&_pthreadRwlock);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"读操做");
// 解锁
pthread_rwlock_unlock(&_pthreadRwlock);
}
// 写操做
- (void)write{
// 写加锁
pthread_rwlock_wrlock(&_pthreadRwlock);
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"写操做");
// 解锁
pthread_rwlock_unlock(&_pthreadRwlock);
}
// ****************打印结果****************
2020-01-03 09:33:53.220426+0800 MultithreadingDemo[61575:5891181] 读操做
2020-01-03 09:33:53.220490+0800 MultithreadingDemo[61575:5891182] 读操做
2020-01-03 09:33:54.221545+0800 MultithreadingDemo[61575:5891182] 读操做
2020-01-03 09:33:54.221570+0800 MultithreadingDemo[61575:5891181] 读操做
2020-01-03 09:33:55.225951+0800 MultithreadingDemo[61575:5891182] 写操做
2020-01-03 09:33:56.231264+0800 MultithreadingDemo[61575:5891181] 写操做
2020-01-03 09:33:57.231891+0800 MultithreadingDemo[61575:5891182] 写操做
2020-01-03 09:33:58.233379+0800 MultithreadingDemo[61575:5891181] 写操做
2020-01-03 09:33:59.235486+0800 MultithreadingDemo[61575:5891181] 读操做
2020-01-03 09:33:59.235522+0800 MultithreadingDemo[61575:5891182] 读操做
2020-01-03 09:34:00.237975+0800 MultithreadingDemo[61575:5891181] 读操做
2020-01-03 09:34:00.237974+0800 MultithreadingDemo[61575:5891182] 读操做
复制代码
dispatch_barrier_async
(异步栅栏函数)GCD中dispatch_barrier_async
的特色就是它不会阻塞当前线程,可是它会阻塞同一个派发队列中在它以后的任务的执行。所以咱们能够将写的操做放在异步栅栏函数中,读操做放在普通的GCD异步函数中。要注意的是,要写栅栏函数生效,必须相关任务都放在同一个派发队列中,而且要是本身建立的并发队列。代码以下所示:
- (void)dispatchBarrierAsync{
_readWriteQueue = dispatch_queue_create("readWriteQueue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 2; i++) {
dispatch_async(_readWriteQueue, ^{
[self read1];
[self read1];
[self write1];
[self write1];
[self read1];
[self read1];
});
}
}
// 读操做
- (void)read1{
dispatch_async(_readWriteQueue, ^{
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"读操做");
});
}
// 写操做
- (void)write1{
dispatch_barrier_async(_readWriteQueue, ^{
[NSThread sleepForTimeInterval:1.0f];
NSLog(@"写操做");
});
}
// ****************打印结果****************
2020-01-03 09:52:23.311609+0800 MultithreadingDemo[61639:5898905] 读操做
2020-01-03 09:52:23.311593+0800 MultithreadingDemo[61639:5898906] 读操做
2020-01-03 09:52:24.316886+0800 MultithreadingDemo[61639:5898905] 写操做
2020-01-03 09:52:25.319288+0800 MultithreadingDemo[61639:5898905] 写操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898905] 读操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898908] 读操做
2020-01-03 09:52:26.321448+0800 MultithreadingDemo[61639:5898906] 读操做
2020-01-03 09:52:26.321452+0800 MultithreadingDemo[61639:5898907] 读操做
2020-01-03 09:52:27.325920+0800 MultithreadingDemo[61639:5898907] 写操做
2020-01-03 09:52:28.330862+0800 MultithreadingDemo[61639:5898907] 写操做
2020-01-03 09:52:29.333543+0800 MultithreadingDemo[61639:5898907] 读操做
2020-01-03 09:52:29.333543+0800 MultithreadingDemo[61639:5898908] 读操做
复制代码