本文主讲多线程相关面试问题:包括GCD、NSOperation、NSThread、多线程与锁。
一、GCD
- 同步/异步 和串行/并发
- dispatch_barrier_async 异步栅栏调用
- dispatch_group
01 异步函数+并发队列:开启多条线程,并发执行任务
02 异步函数+串行队列:开启一条线程,串行执行任务
03 同步函数+并发队列:不开线程,串行执行任务
04 同步函数+串行队列:不开线程,串行执行任务
05 异步函数+主队列:不开线程,在主线程中串行执行任务
06 同步函数+主队列:不开线程,串行执行任务(注意死锁发生)
注意同步函数和异步函数在执行顺序上面的差异
1、同步/异步 和 串行/并发
同步/异步 和 串行/并发
+ 同步分配任务到串行队列
dispatch_sync(serial_queue,{//任务});
+ 异步分配任务到串行队列
dispatch_async(serial_queue,{//任务});
+ 同步分配任务到并发队列
dispatch_sync(concurrent_queue,{//任务});
+ 异步分配任务到并发队列
dispatch_async(concurrent_queue,{//任务});
(1)、同步串行 dispatch_sync(serial_queue , ^{ //任务 });
同步主线程
//同步主线程 死锁
- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
死锁原因
结果:造成死锁 队列引起的循环等待.
在主队列中提交了viewDidLoad,然后又提交了block。因此在执行viewDidLoad过程中,需要调用block,block完成之后,viewDidLoad才能继续往下执行,而block因为队列先进先出的性质必须要等viewDidLoad执行结果才能调用,导致相互等待情况,从而死锁.
同步串行
- (void)viewDidLoad {
dispatch_sync(serialQueue, ^{
[self doSomething];
});
}
同步串行分析
结果:顺序执行 都在主线程,不开辟新线程
(2)、同步并发 dispatch_sync(concurrent_queue , ^{ //任务 });
- (void)viewDidLoad {
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"2");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
答案:12345
-
顺序执行 都在主线程 不开辟新线程
ps:如果2 3 都是添加到同一串行队列 就会造成死锁 23循环等待 -
只要同步方式提交任务,无论串行还是并发都是在当前线程执行
-
dispatch_sync() 在当前主线程执行
(3)、异步串行 dispatch_async(serial_queue , ^{ //任务 });
异步主队列
- (void)viewDidLoad {
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
- 顺序执行,都在主线程,不开辟新线程
(4)、异步并发 dispatch_async(concurrent_queue , ^{ //任务 });
//腾讯面试题:
- (void)viewDidLoad {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"1");
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"3");
});
}
- (void)printLog{ NSLog(@"2"); }
- 输出: 13
- 该题涉及知识点较多:GCD、线程的Runloop、performSeletor内部实现
- 异步分派到全局队列中,GCD底层所分派的线程默认是不开启对应runloop的,而performSeletor:即使是延迟0秒,也是需要提交任务到runloop的逻辑,所以performSeletor方法会失效的
- performSeletor:方法要想有效执行, 必须是方法调用所属线程是有runloop的,没有就会失效
2、dispatch_barrier_async()
多读单写方案:dispatch_barrier_async(concurrent_queue , ^{ //写操作 });
多读单写方案 多读单写实现问题:怎么利用GCD实现多读单写?或者说想要实现多读单写,怎么去实现?
答:
- 读者与读者并发(读操作添加到并发队列同步访问)
- 读者与写者、写者与写者互斥 (写操作通过dispatch_barrier_async+ 异步栅栏添加到并发队列中)
@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、dispatch_group_async()
问题:
使用GCD实现:A、B、C三个任务并发,完成后执行任务D?
答: 所有异步任务添加到并发队列中,然后使用dispatch_group_notify函数,来监听前面多个的任务是否完成,如果完成, 就会调用dispatch_group_notify中的block
(参考代码实例)
二、NSOperation
需要和NSOperationQueue配合使用来实现多线程方案。
1、特点:添加任务依赖、任务执行状态监控、最大并发数。
2、任务执行状态控制:isReady、isExecuting、isFinished、isCancelled。
3、状态监控
- 如果只重写
main
方法,底层控制变更任务执行完成状态,以及任务退出。 - 如果重写
start
方法,需自行控制任务状态。
4、系统是怎样移除一个isFinished=YES的NSOperation的?
答:通过KVO的方式,通知对应的NSOprationQueue达到对NSOperation对象进行移除。
三、NSThread
考查面试题:
1> 如何NSThread结合runloop实现常驻线程
2> NSThread 的 内部实现机制, start方法实现逻辑流程
(结合 gnustep-base-1.24.9 源码分析)
问题: NSThread启动流程?
答: start() ——>创建pthread线程——>main()——>[target performSelector:selector]——>exit()
问题: 如何通过runloop和NSThread实现一个常驻线程?
从下一章runloop中寻找答案
问题: NSThread执行原理是怎样的?
答: 实际内部创建一个pthread线程 ,当main()函数或者指定的target 的selector方法执行结束后,系统会为我们进行线程的退出管理操作. 如果需要维护一个常驻线程,需要NSThread所对应的selector方法中维护runloop事件循环。
四、多线程与锁
iOS中有哪些锁?
- @synchronized
- atomic
- OSSpinLock
- NSRecursiveLock
- NSLock
- dispatch_semaphore_t
1、 @synchronized的使用场景
一般在创建单例的时候使用,保证在多线程环境下创建的对象是唯一的
2、atomic
- 修饰属性的关键字
- 对被修饰的对象进行原子操作(不负责使用,只负责赋值)
@property(atomic)NSMutableArray *array;
self.array = [NSMutableArray array]; //array赋值操作,能保证线程安全
[self.array addObject:obj]; // array使用, 不能保证线程安全,需要额外做线程安全保护
3、OSSpinLock自旋锁
-
循环等待
询问,不释放当前资源 - 用于轻量级数据访问,简单的int值+1/-1操作
没有具体用过,但是可以通过分析runtime源码来学习系统关于OSSpinLock自旋锁的使用情况.
4、 NSLock
- 一般用于解决细粒度的线程同步问题, 来保证各个线程互斥,进入自己的临界区.
- (void)methodA {
[lock lock];
[self methodB];
[lock unlock];
}
- (void)methodB {
[lock lock];
//操作逻辑
[lock unlock];
}
- 该写法重入的原因 会导致死锁!
- 解决: 通过NSRecursiveLock递归所可以解决
5、NSRecursiveLock 递归锁
- NSRecursiveLock 递归锁特性: 可以重入
- (void)methodA {
[recursiveLock lock];
[self methodB];
[recursiveLock unlock];
}
- (void)methodB {
[recursiveLock lock];
//操作逻辑
[recursiveLock unlock];
}
6、dispatch_semaphore_t 信号量
-
dispatch_semaphore_t 信号量 也是用来实现线程同步, 包括对共享资源互斥访问的信号量机制,类似于计算机专业的记录型信号量
-
创建信号量 dispatch_semaphore_create(1)
//dispatch_semaphore_create内部实现 实例化一个结构体
struct semaphore{
int value; // 信号量的值
List<thread>; // 线程的进程控制表pcd 或者一些其他线程的一个唯一标识所维护的一个线程列表
}
- dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER)
信号量-1,阻塞是一个主动行为
//dispatch_semaphore_wait() 实现逻辑
{
S.value = S.value - 1;
if S.value < 0 then Block(S.List); //阻塞是一个主动行为
}
- dispatch_semaphore_signal(semaphore) 信号量+1,唤醒是一个被动行为
//dispatch_semaphore_signal()实现逻辑
{
S.value = S.value + 1;
if S.value <= 0 then wakeup(S.List); //唤醒是一个被动行为
}
五 多线程相关面试问题
1、怎样用GCD实现多读单写?
答: dispatch_barrier_async()的使用.
- 读者与读者并发(读操作添加到并发队列同步访问)
- 读者与写者、写者与写者互斥 (写操作通过dispatch_barrier_async+ 异步栅栏添加到并发队列中)
2、iOS系统为我们提供的几种多线程技术各自的特点是怎么样的?
答案:GCD、NSOperation、NSThread。
- GCD 用来实现简单的线程同步,包括子线程的分派,包括实现多读单写场景的解决
- NSOperation及NSOperationQueue 比如AFNetworking、SDWebImage都会涉及到NSOpration,由于它的特点是方便我们对任务的状态进行控制,包括可以控制添加依赖、移除依赖
- NSThread 一般用它来实现一个常驻线程
3、NSOperation对象在Finished之后是咋样从queue当中移除掉的?
答: NSOperation对象在Finished之后, 通过KVO的方式,通知对应的NSOprationQueue达到对NSOperation对象进行移除。
4、你都用过哪些锁?结合实际谈谈你是怎样使用的?
答:
网友评论