1.0 GCD
同步/异步 和 串行/并发
dispatch_barrier_async 栅栏函数,解决多读单写的问题
dispatch_group
问题1:同步串行问题,以下片段会造成什么问题?如何解决?
- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomeThing];
});
}
会造成死锁。队列引起的循环等待
完整回答:主队列提交了viewDidLoad一个任务一个blcok任务,无论哪个任务,最终都要分派到主线程 去处理,比如分派viewDidLoad到主线程中去处理,在它执行过程当中他需要调用block,当block同步调用完成之后,这个viewDidLoad才能继续向下,所以viewDidLoad任务需要依赖于后续提交的block任务,block要想执行,要先遵循队列的先进先出的性质,所以block依赖于viewdidload的完成 产生了相互等待
问题2:同步串行问题 以下片段会造成什么问题?如何解决?
- (void)viewDidLoad {
dispatch_sync(serialQueue, ^{
[self doSomeThing];
});
}
没问题
完整回答:viewDidLoad任务执行到某一刻的时候需要同步提交一个任务到对应的串行队列,由于同步方式提交的任务,同步提交在当前线程执行,串行队列所提交的block任务也是在主线程中执行,串行队列所提交的任务在主线程中执行完成后,在去主队列viewDidLoad方法后续的方法执行
如果没在viewDidLoad执行就会产生死锁和第一个问题一样的。同步分派到主队列会产生队列引起的循环等待死锁问题。
问题3:同步并发问题
完整回答:只要是同步方式提交任务,无论提交串行队列还是并发队列,都是在当前线程去执行。所以打印12345 如果把上图的并发队列改为自定义的串行队列那么就产生死锁问题。
问题4:异步串行问题
完整回答: 通过异步方式分派到全局并发队列之后,这个block会在GCD底层所维护的线程池中某一个线程去进行执行处理,而关于GCD底层所分派的线程默认情况下是没有开启对应RunLoop的,performSelector有效执行必须是它方法调用所属当前线程的有RunLoop的,没有的话就会失效。
2.0 dispatch_barrier_async()
问题1:怎样利用GCD实现多读单写?让你实现一个多读单写的模型如何实现?
完整答案:代码实现片段
@interface UserCenter()
{
// 定义一个并发队列
dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问
NSMutableDictionary *userCenterDic;
}
@end
// 多读单写模型
@implementation UserCenter
- (id)init
{
self = [super init];
if (self) {
// 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
//读取操作
- (id)objectForKey:(NSString *)key
{
__block id obj;
// 同步读取指定数据
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
//写入操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 异步栅栏调用设置数据
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
@end
3.0 dispatch_group
问题1:使用GCD实现这个需求,A,B,C 三个任务并发任务,完成后执行任务D
完整代码:代码实现片段
@interface GroupObject()
{
dispatch_queue_t concurrent_queue;
NSMutableArray <NSURL *> *arrayURLs;
}
@end
@implementation GroupObject
- (id)init
{
self = [super init];
if (self) {
// 创建并发队列
concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
arrayURLs = [NSMutableArray array];
}
return self;
}
- (void)handle
{
// 创建一个group
dispatch_group_t group = dispatch_group_create();
// for循环遍历各个元素执行操作
for (NSURL *url in arrayURLs) {
// 异步组分派到并发队列当中
dispatch_group_async(group, concurrent_queue, ^{
//根据url去下载图片
NSLog(@"url is %@", url);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 当添加到组中的所有任务执行完成之后会调用该Block
NSLog(@"所有图片已全部下载完成");
});
}
@end
4.0 NSOperation
需要和NSOperationQueue配合使用来实现多线程方案
添加任务依赖
任务执行状态控制
最大并发量
任务执行状态控制
isReady 当前任务是否处于就绪状态
isExecuting 当前任务是否处于正在执行中状态
isFinished 当前任务是否已经执行完成
isCancelled 当前任务是否已取消
状态控制
1:如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
2:如果重写了start方法,自行控制任务状态。底层实现-创建一个自定释放池,获取线程的优先级,状态的异常判断,如果都通过的话会判断当前状态是否处于执行中,如果没有执行的话去设置执行中的状态,会判断当前任务是否有被取消,如果没有被取消调用 [self main]函数,之后调用[self _finish]方法(kvo的方式变更finish的标记位系统会在合适的时机通过NSOperationQueue来监听kvo键值对,在合适的时机把NSOperation移除调),最后自动释放池的release的操作。
问题1:系统是怎样移除一个isFinished=YES的NSOperation的?
答案:通过kvo
5.0 NSThread
问题1:Start方法实现内部原理?
首先做异常判断处理,之后调用pthread_create函数创建线程同时指定这个线程的启动函数,获取启动函数t,发送通知通知观察者当前线程已经启动,设置线程名称,调用线程main函数,最后调用关闭函数来关闭。
如果想做一个长驻线程的效果,需要在main函数中做一个RunLoop的循环,main函数当中先做了一个异常判断,之后会通过调用target的selector来执行创建NSThread所指定的选择器,可以通过main函数里面调用的target的selector当中去维护这个事件循环,来达到常驻线程的目的
6.0 有哪些锁
问题1:iOS当中都有哪些锁?
@synchronized 一般在创建单例对象的时候使用(保证在多线程环境下创建对象是唯一的)
atomic 修饰属性的关键字,对被修饰对象进行原子操作(不负责使用)
OSSpinLock(自旋锁) 循环等待访问,不释放当前资源。用于轻量级数据访问,简单的int值+1/-1操作
NSLock 一般是用于解决细力度的线程同步问题,来保证各个线程互斥进入自己的临界区。
NSrecursiveLock(递归锁) 特点是可以重入
dispatch_semaphore_t(信号量)
自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的
互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成
读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现
多线程面试问题总结
问题1:怎样用GCD实现多读单写?
@interface UserCenter()
{
// 定义一个并发队列
dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问
NSMutableDictionary *userCenterDic;
}
@end
// 多读单写模型
@implementation UserCenter
- (id)init
{
self = [super init];
if (self) {
// 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
//读取操作
- (id)objectForKey:(NSString *)key
{
__block id obj;
// 同步读取指定数据
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
//写入操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 异步栅栏调用设置数据
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
@end
问题2: iOS系统为我们提供了几种多线程技术各自的特点是怎样的?
GCD,NSOperation,NSThread
一般使用GCD实现一些简单的线程同步,子线程的分派,实现类似于多读单写场景的解决。
NSOperation 比如说第三方框架AFN,SDWebImageView,他里面都会涉及NSOperation,由于它的特点可以方便对任务的状态进行控制,包括可以控制添加依赖移除依赖。
NSThread 一般用于实现常驻线程。
他们的特点就要结合实际的应用场景采取对应的方案。
问题3:NSOperation对象在Finished之后是怎样从队列当中移除掉的?
会通过kvo的方式通知他所对应的NSOperationQueue达到对对象移除的目的。
问题4:你都用过哪些锁?结合实际谈谈你是怎样使用的?
递归锁,在进行多线程同步的时候,遇到锁重入问题就可以采用递归锁。
线程安全问题
线程锁的实现机制其实基于很简单的概念:标志是否被占用
OSSpinLock
: 自旋锁,效率高,占用CPU,废弃了,会产生优先级反转,导致优先级较高的线程时间更多,而被锁线程时间更少dispatch_semaphore
: 信号量, 信号量数值觉得最多能进入的线程树,一般设置为1.NSLock
: 互斥锁,对pthread普通锁的封装NSRecursiveLock
: 递归锁,也是互斥锁, 解决递归调用死锁问题.对pthread递归锁的封装.NSCondition
: 对pthread条件与锁的封装.NSConditionLock
: 对NSCondition的进一步封装,可以设置具体的条件.@synchorized
: 同步,可以对代码块使用.
-
互斥锁应当是排它的,意思是锁在被某个线程获取之后,只有获取锁的线程才能释放这个锁。其他线程必须等到获取锁的线程不再拥有锁之后,才能继续执行。
-
信号量拥有比互斥锁更多的用途。当信号量的
value
大于0时,所有的线程都能访问临界资源。在线程进入临界区后,value
减一,反之亦然。如果信号量初始化为0
时,可以看做是等待任务执行完成而非资源保护。value
的操作应当是采用原子操作
来保证指令的安全性的 -
自旋实现的线程锁来说,存在一个线程间共享的标记变量。当某个线程进入临界区后,变量被标记,此时其他线程再想进入临界区,会进入
while
循环中空转等待 -
线程锁的实现机制其实基于很简单的概念:
标志是否被占用
问题5: GCD如何实现最大并发数呢?
GCD 可以用信号量 semaphore 来实现
//建立任务组
dispatch_group_t group = dispatch_group_create();
//设置信号量大小
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
//创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//添加实现任务
for (int i = 0; i < 100; i++) {
//semphore值为0时会一直等待执行。当>=1时执行下面的代码。可通过一次,会将semphore的值减1.
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//监视queue队列
dispatch_group_async(group, queue, ^{
NSLog(@"这里在执行耗时操作----%i",i);
sleep(2);
//当前线程执行完成后,使semaphore的值加1,这样,如果semphore的值就加1,会触发dispatch_semaphore_wait执行一条代码
dispatch_semaphore_signal(semaphore);
});
}
//永远地监听Group中的任务是否执行完成。
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);