内容简介:iOS开发中经常要使用到多线程,在面试的时候也是经常问到,比较常见的面试题有下面这些:GCD中有两个用来执行任务的函数:面试题一:以下代码在主线程中执行,是否会产生死锁?
iOS开发中经常要使用到多线程,在面试的时候也是经常问到,比较常见的面试题有下面这些:
-
iOS的多线程方案有哪几种?你更倾向于哪一种?
-
GCD的队列类型。
-
说一下OperationQueue和GCD的区别,以及各自的优势。
-
线程安全的处理手段有哪些?
-
OC你了解的锁有哪些?在此基础上进行二次提问“
1.自旋和互斥对比
2.使用以上锁需要注意哪些?
3.用C/OC/C++,任选其一,实现自旋或互斥。
下面就带着这些问题,来总结一下多线程的相关问题。
iOS中的常见多线程方案
技术方案 | 简介 | 语言 | 线程声明周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API,适用于Unix\Linux\windows等系统,跨平台,可移植,使用难度大 | C | 程序员管理 | 几乎不用 |
NSThread | 使用更加面向对象,简单易用,可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | 旨在替代NSThread等线程技术,充分利用设备的多核 | C | 自动管理 | 经常使用 |
NSOperation | 基于GCD,比GCD多了一些更简单实用的功能,使用更加面向对象 | OC | 自动管理 | 经常使用 |
GCD基础回顾
GCD中有两个用来执行任务的函数:
-
用同步的方式执行任务
即在当前线程中去做事情
dispatch_sync(dispatch_queue_t _Nonnull queue, ^{})
-
用异步的方式执行任务
即另外开线程去做事情
dispatch_async(dispatch_queue_t _Nonnull queue, ^{})
-
并发队列:
可以让多个任务同时执行(自动开启多个线程同时执行任务)
并发功能只有在异步函数下才有效
-
串行队列:
让任务一个接着一个的执行(只会开启一条线程,一个任务执行完毕后,再执行下一个任务)
同步,异步,串行,并行
-
同步和异步的主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务。具备开启新线程的能力
-
并发和和串行的主要影响:任务的执行方法
并发:多个任务同时执行( 会开启多条线程 )
串行:一个任务执行完毕后,再执行下一个任务( 只会开启一个线程 )
下面是各种队列的执行效果:
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
GCD中死锁的问题
面试题一:以下代码在主线程中执行,是否会产生死锁?
- (void)viewDidLoad { [super viewDidLoad]; //问题:以下代码在主线程中执行,会不会产生死锁? dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ NSLog(@"执行任务2"); }); NSLog(@"执行任务3"); }
我们运行代码,发现产生了崩溃,说明产生了死锁。下面来分析一下为什么会产生死锁:
我们知道, dispatch_sync()
是同步执行,不会开辟新线程,并且要 dispatch_sync()
的block执行完了才会继续往下执行,所以任务2是加入到了主队列中。 主队列是串行队列,,所以会串行执行加入其中的任务,等一个任务执行完了再执行另外一个,在任务2加入主队列之前,viewDidLoad这个大任务已经加入了主队列,所以任务2要等ViewDIdLoad执行完,才会执行任务2,也就是等到任务3执行完,才会执行任务2,但是由于任务3在任务2后面,所以要等到任务2执行完了,才执行任务3。这样就造成了任务2等任务3,任务3等任务2,造成死锁。
面试题二:以下代码是否会发生死锁:
- (void)viewDidLoad { [super viewDidLoad]; //问题:以下代码在主线程中执行,会不会产生死锁? dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{ NSLog(@"执行任务2"); }); // NSLog(@"执行任务3"); }
运行程序,发现程序并没有崩溃,并且产生打印:
2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 执行任务3 2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 执行任务2
为什么把同步改成异步,就不死锁了呢?我们分析一下, 由于队列是主队列,一定是把任务2加到主队列中,并且在此之前viewDidLoad的任务已经加入到了主队列中,所以要viewDidLoad执行完了才能执行任务2,由于dispatch_async()是异步执行,所以不用等到任务2执行完了再执行任务3,可以直接执行任务3,任务3执行完了,viewDidLoad也就执行完了,也就可以执行任务2了,所以打印的结果一定是先执行任务3再执行任务2。
面试题3:以下代码是否会发生死锁:
- (void)viewDidLoad { [super viewDidLoad]; //问题:以下代码在主线程中执行,会不会产生死锁? dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{//1 NSLog(@"执行任务2"); dispatch_sync(queue, ^{ NSLog(@"执行任务3"); }); NSLog(@"执行任务4"); }); NSLog(@"执行任务3"); }
运行代码,发现在dispatch_sync这里产生了崩溃,打印结果如下:
2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 执行任务3 2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 执行任务2
下面分析一下为什么会产生死锁:
dispatch_async是异步执行,所以先打印了 执行任务3
,然后把1这个block加入了串行队列中,这时串行队列中有1这个block,然后又向串行队列中加入了任务3,任务3需要同步执行,所以任务3执行完了才会执行任务4,由于串行队列中1这个block排在任务3前面,所以要1这个block完成才会执行任务3,也就是要任务4执行完了才会执行任务3,而任务4又要等到任务3执行完成,这样互相等待,造成死锁。
总结
造成死锁的条件1是同步,2是往当前的串行队列中添加事件。
直接获取全局队列和手动创建队列的关系
看下面一段代码:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_queue_t queue4 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT); dispatch_queue_t queue5 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT); NSLog(@"%p, %p, %p, %p, %p", queue1, queue2, queue3, queue4, queue5);
打印结果:
0x1062e2500, 0x1062e2500, 0x1062e2680, 0x600000146bf0, 0x600000146ca0
queue1,queue2,queue3是直接获取的全局队列,从打印结果可以看出,如果优先级相同,则获取的是同一个队列,如果优先级不同,则获取的是不同的队列。queue4,queue5是手动创建的队列,即便它们的identifier相同,但是仍然创建了不同的队列。
面试题
面试题一:问下列代码的打印结果:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue1, ^{ NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3"); });
- (void)test{ NSLog(@"2"); }
我们先看一下打印结果:
很奇怪,2压根就没有打印,也就是压根就没有执行test方法,这是为什么呢?
我们把
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
改成
[self performSelector:@selector(test) withObject:nil];
看看打印结果:
说明这样是能成功执行test函数的,我们在runtime中找一下 - (id)performSelector:(SEL)aSelector withObject:(id)object;
的源码:
- (id)performSelector:(SEL)sel withObject:(id)obj { if (!sel) [self doesNotRecognizeSelector:sel]; return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj); }
可以看到,就是简单的调用 objc_msgSend()
。但是在runtime的源码中却没有找到 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
的实现。
我们再修改一下代码,让其直接在主线程中执行:
NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");
打印结果:
那么就有理由猜测 NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");
的执行和线程有关。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
这句代码的本质是往runloop中去添加一个NSTimer,由于主线程中有runloop,所以可以正常执行,但是在子线程中默认是没有启动runloop的,所以NSTimer也就没有办法成功执行。
我们可以启动子线程中的runloop试一下:
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue1, ^{ NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3"); [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; });
打印结果:
GNUstep
Foundation框架是不开源的,所以我们想看其中的源码是看不到的。GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然它不是苹果官方的完整实现,但是和官方的实现十分接近,区别不大,在学习的时候我们可以用来作为参考。
源码地址: http://www.gnustep.org/resources/downloads.php
我们下载了GNUstep的代码后,打开找到Foundation文件夹,在这个文件夹下找到 NSRunLoop.m
这个文件,在这个文件中找到 - (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds
这个方法的实现:
- (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds { NSRunLoop *loop = [NSRunLoop currentRunLoop]; GSTimedPerformer *item; item = [[GSTimedPerformer alloc] initWithSelector: aSelector target: self argument: argument delay: seconds]; [[loop _timedPerformers] addObject: item]; RELEASE(item); [loop addTimer: item->timer forMode: NSDefaultRunLoopMode]; }
GCD队列组
思考:如何用gcd实现以下功能:
异步并发执行任务1,任务2
等任务1,任务2都执行完毕后,再回到主线程执行任务3
我们可以用dispatch_group_t和dispatch_group_notify来完成这个功能:
dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务1-%@", [NSThread currentThread]); } }); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务2-%@", [NSThread currentThread]); } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务3-%@", [NSThread currentThread]); } });
如果需要在任务1和任务2完成之后再完成任务3,任务4,可以这样:
dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务1-%@", [NSThread currentThread]); } }); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务2-%@", [NSThread currentThread]); } }); dispatch_group_notify(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务3-%@", [NSThread currentThread]); } }); dispatch_group_notify(group, queue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"任务4-%@", [NSThread currentThread]); } });
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【重回基础】线程池源码剖析:Worker工作线程
- Java 多线程(五)—— 线程池基础 之 FutureTask源码解析
- JStorm 源码解析:基础线程模型
- EventBus源码剖析(3) — 线程模式
- 线程池的使用和源码剖析
- Java 并发编程 -- 线程池源码实战
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
移动Web前端高效开发实战
iKcamp / 电子工业出版社 / 2017-9 / 89.00
移动互联网的兴起和快速普及,给前端开发人员带来了前所未有的新机遇。移动Web前端技术作为整个技术链条中重要的一环,却乱象丛生。《移动Web前端高效开发实战:HTML 5 + CSS 3 + JavaScript + Webpack + React Native + Vue.js + Node.js》是一本梳理移动前端和Native客户端技术体系的入门实战书。 《移动Web前端高效开发实战:HTML......一起来看看 《移动Web前端高效开发实战》 这本书的介绍吧!