接着前面RunLoop的应用中,分享了关于线程保活的知识点,本文再继续由浅入深的写一写多线程的东西。
多线程也是面试中被问到概率很大的知识点,也是我们作为一名iOS开发者必须掌握的内容。本文我想以问答的形式,分享记录我对多线程的理解。
常见的多线程方案?
- pthread: 一套通用的C语言API,适用于Unix、Linux、Windows等系统,可跨平台,可移植,但是使用难度大
- NSThread:在RunLoop的应用中的示例代码中使用过,简单易用,可以直接操作线程对象
- GCD:旨在替代NSThread等线程技术,也是C语言的API
- NSOperation: 基于GCD封装的一套OC语言的API
下面对于常见的多线程方法,总结了一个对比的表格,帮助我们加深记忆:
上述几种常见的多线程方案,除pthread之外,其他几种底层都是基于pthread实现的。
根据上面的表格,以及平时工作的经验,我们也可以知道,开发中最常使用的是GCD和NSOperation这两种多线程方案,因为他们都使用简单,而且不需要程序员管理线程的生命周期。
GCD
先说一说GCD中常用的函数,GCD中有两个用来执行任务的函数,dispatch_sync是用同步的方式执行任务,同步,就是在当前线程中执行任务
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async用异步的方式执行任务,异步,就是可能会创建新线程并且可以在新线程中执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
其中queue是队列,block中就是要执行的任务。
队列的类型
队列可以分为串行和并发队列两大类,队列的类型决定了任务的执行方式,或者说是执行顺序,是并发或者串行执行。
其实还有主队列,主队列是一种特殊的串行队列。
再做一下名词解释吧,所谓并发,就是多个任务同时执行;而串行呢,是要一个任务执行完,再执行下一个任务。
是否开启新线程
dispatch_sync 和 dispatch_async , 决定了是否需要开启新的线程,dispatch_sync不需要开启新线程,dispatch_async则需要开启新线程,但是也不是绝对,比如使用了主队列。
队列的类型以及同步和异步,共同决定了在执行任务的时候,是否开启新线程,以及执行任务的方式。
总结一下:同步都是在当前线程(不需要开启新线程)中串行执行任务的,异步在主队列这个特殊队列中没有开启新线程,其他队列都会开启新线程,但是执行方式,由所在队列决定。
死锁
关于死锁的问题,但凡面试被问到多线程,十有八九要继续说一个场景,或者给一段代码,让你判断是否会发生死锁,以及发生死锁的原因。
其实产生死锁的原因可以总结为:调用dispatch_sync(同步的)往当前串行队列中添加任务,就会产生死锁。同步和串行队列的特点决定了会产生死锁。
下面举几个例子,每一段代码(默认在主线程调用),都分析一下是否会产生死锁:
- (void)interview01 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
以上代码在主线程执行,会产生死锁嘛? 会
队列的特点:FIFO 先进先出, 本来主队列里面就有一个任务viewDidLoad 需要等待viewDidLoad执行完才能执行下一个任务
dispatch_sync 需要立马在当前线程执行任务, 执行完才能往下执行
总结:想要执行任务2 , 必须要等任务3(viewDidLoad)执行完, 而想要任务3执行完 又必须要任务2(dispatch_sync)执行完,所以产生死锁。
继续看下一个例子:
- (void)interview02 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
// dispatch_async 不要求立马在当前线程执行任务 可以等上一个任务(interview02)执行完再继续执行
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
以上代码在主线程执行,会产生死锁嘛? 不会
dispatch_async 不要求立马在当前线程执行任务 可以等上一个任务(interview02)执行完再继续执行
继续看下一个例子:
- (void)interview03 {
NSLog(@"执行任务1");
// 串行队列
dispatch_queue_t queue = dispatch_queue_create(@"myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
以上代码在主线程执行,会产生死锁嘛? 会
根据文章上一节贴的那张关于同步/异步和队列的表格,可知在手动创建的串行队列中,调用dispatch_async虽然开启了新线程,但也是串行执行,再通过dispatch_sync添加同步任务,依然会造成死锁。
对于interview03中的死锁,可以有两种解决方式:
- 任务3放在并发队列中
- 任务3放在其他的串行队列中
- (void)interview03 {
NSLog(@"执行任务1");
// 串行队列
dispatch_queue_t queue = dispatch_queue_create(@"myqueue", DISPATCH_QUEUE_SERIAL);
// 并发队列
dispatch_queue_t queue2 = dispatch_queue_create(@"myqueue2", DISPATCH_QUEUE_CONCURRENT);
// 另外一个串行队列
dispatch_queue_t queue3 = dispatch_queue_create(@"myqueue3", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
// dispatch_sync(queue, ^{
// NSLog(@"执行任务3");
// });
// 换成并发队列 就不会产生死锁 放在不同的队列中 就不存在互相等待的问题
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
// 换一个串行队列 也不会产生死锁
dispatch_sync(queue3, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
继续看下一个例子:
- (void)interview04 {
NSLog(@"执行任务1");
// 并发队列
dispatch_queue_t queue = dispatch_queue_create(@"myqueue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
以上代码在主线程执行,会产生死锁嘛?不会
并发队列 不会阻塞
根据对上面几个例子的分析,是不是也验证了,产生死锁的原因。再复习一遍产生死锁的原因总结为:调用dispatch_sync(同步的)往当前串行队列中添加任务,就会产生死锁。
RunLoop和线程
这个知识点算是复习吧,前面RunLoop的应用里说过的内容,这里换一种形式被问到,可能会有点儿迷糊,直接看下面这段代码
- (void)test {
NSLog(@"2");
}
- (void)interview05 {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
这个问题是问最终的打印结果,test方法中的2是打印不出来的,结果只打印了1 和 3.
原因是performSelector: withObject:afterDelay:这个方法,他并不能等价于[self test]这种调用方式, 这个方法是定义在NSRunLoop.h文件中的,底层实现是设置了一个定时器,然后需要把定时器添加到当前线程的runloop中,RunLoop的应用中说过,子线程默认是不会自动创建runloop的,所以这里通过performSelector: withObject:afterDelay:这个方法调用test 是无效的。
替换成下面这句代码,就可以打印出 1 2 3 ,因为他是等价于[self test]的
[self performSelector:@selector(test) withObject:nil];
这两个方法的申明很像,就差了最后一个参数,但是performSelector:withObject:是定义在NSObject中的。
还有一种方案,那就是使用线程保活,也可以让test方法执行,添加下面两句代码到block中
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
再继续分析,如果是放在主线程中又会是什么打印结果呢?
NSLog(@"1");
// 往主runloop中添加了定时器
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
测试之后发现,打印结果是 1 3 2,这里就要根据之前分享过的RunLoop的原理中说过的处理逻辑来分析了,虽然delay是0,也是要等到runloop来处理定时器,才能执行。
写完了这篇分享的文章,对于多线程和runloop,以及多线程和runloop之间的关系理解的也更加深刻了。学习就是要这样一步步来,然后多复习,温故真的能知新。