一、多线程的选择方案

技术方案简介语言线程生命周期使用评率
pthread一套通用的多线程API适用于Unix/Linux/Windows等系统跨平台/可移植使用难度大C程序员管理几乎不用
NSThread使用更加面向对象简单易用,可直接操作线程对象OC程序员管理偶尔使用
GCD旨在替代NSThread等线程技术充分利用设备的多核C自动管理经常使用
NSOperation基于GCD(底层是GCD)比GCD多了一些更简单实用的功能使用更加面向对象OC自动管理经常使用

注意:如果使用NSThread的performSelector:withObject:afterDelay:时需要添加到当前线程的runloop中,因为在内部会创建一个NSTimer

二、GCD和NSOperation的比较

  • GCDNSOperation的关系如下:

    • GCD是面向底层的C语言的API
    • NSOperation是用GCD封装构建的,是GCD的高级抽象
  • GCDNSOperation的对比如下:

  1. GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构——写起来更加方便
  2. GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序
  3. NSOpration甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier任务才能控制执行顺序,较为复杂
  4. NSOperation支持KVO(面向对象)可以检测operation是否正在执行、是否结束、是否取消
  • 实际项目中,很多时候只会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的是优化完善、运行快速的GCD
  • 如果考虑异步操作之间的事务性、顺序性、依赖关系,比如多线程并发下载,GCD需要写更多的代码来实现,而NSOperation已经内建了这些支持
  • 不管是GCD还是NSOperation,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建、调度管理和释放都做得很好;而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

三、多线程的应用场景

  • 异步执行
    • 将耗时操作放在子线程中,使其不阻塞主线程
  • 刷新UI
    • 异步网络请求,请求完毕dispatch_get_main_queue()回到主线程刷新UI
    • 同一页面多个网络请求使用dispatch_group统一调度刷新UI
  • dispatch_once
    • 单例中使用,一个类仅有一个实例且提供一个全局访问点
    • method-Swizzling使用保证方法只交换一次
  • dispatch_after将任务延迟加入队列
  • 栅栏函数可用作同步锁
  • dispatch_semaphore_t
    • 用作锁保证线程安全
    • 控制GCD的最大并发数
  • dispatch_source定时器替代误差较大的NSTimer
  • AFNetworkingSDWebImage等知名三方库中的NSOperation使用
  • ...

四、线程池的原理

  • 线程池大小小于核心线程池大小
    • 创建线程执行任务
  • 线程池大小大于等于核心线程池大小
  1. 先判断线程池工作队列是否已满
  2. 若没满就将任务push进队列
  3. 若已满时,且maximumPoolSize>corePoolSize,将创建新的线程来执行任务
  4. 反之则交给饱和策略去处理
参数名代表意义
corePoolSize线程池的基本大小(核心线程池大小)
maximumPool线程池的最大大小
keepAliveTime线程池中超过corePoolSize树木的空闲线程的最大存活时间
unitkeepAliveTime参数的时间单位
workQueue任务阻塞队列
threadFactory新建线程的工厂
handler当提交的任务数超过maxmumPoolSize与workQueue之和时,任务会交给RejectedExecutionHandler来处理

饱和策略有如下四个:

  • AbortPolicy直接抛出RejectedExecutionExeception异常来阻止系统正常运行
  • CallerRunsPolicy将任务回退到调用者
  • DisOldestPolicy丢掉等待最久的任务
  • DisCardPolicy直接丢弃任务

五、栅栏函数异同以及注意点

栅栏函数两个API的异同

  • dispatch_barrier_async:可以控制队列中任务的执行顺序
  • dispatch_barrier_sync:不仅阻塞了队列的执行,也阻塞了线程的执行

栅栏函数注意点

  1. 尽量使用自定义的并发队列:
  • 使用全局队列起不到栅栏函数的作用
  • 使用全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
栅栏函数只能控制同一并发队列:打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这有个iOS交流群:642  363  427,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术,iOS开发者一起交流学习成长!

六、栅栏函数的读写锁

多读单写功能指的是:可以多个读者同时读取数据,而在读的时候,不能写入数据;在写的过程中不能有其他写者去写。即读者之间是并发的,写者与其他写者、读者之间是互斥的

- (id)readDataForKey:(NSString*)key {    __block id result;    dispatch_sync(_concurrentQueue, ^{        result = [self valueForKey:key];    });    return result;}- (void)writeData:(id)data forKey:(NSString*)key {    dispatch_barrier_async(_concurrentQueue, ^{        [self setValue:data forKey:key];    });}
  • 读:并发同步获取到值后返回给读者
    • 若使用并发异步则会先返回空的result 0x0,再通过getter方法获取到值
  • 写:写的那个时间段,不能有任何读者+其他写者
    • dispatch_barrier_async满足:等队列中前面的读写任务都执行完了再来执行当前任务

七、GCD的并发量

不同于NSOperation中可以通过maxConcurrentOperationCount去控制并发数,GCD需要通过信号量才能达到效果

dispatch_semaphore_t sem = dispatch_semaphore_create(1);dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);for (int i = 0; i < 10; i++) {    dispatch_async(queue, ^{        NSLog(@"当前%d----线程%@", i, [NSThread currentThread]);        // 打印任务结束后信号量解锁        dispatch_semaphore_signal(sem);    });    // 由于异步执行,打印任务会较慢,所以这里信号量加锁    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);}--------------------输出结果:-------------------当前1----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}当前0----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}当前2----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}当前3----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}当前4----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}当前5----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}当前6----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}当前7----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}当前8----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}当前9----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}--------------------输出结果:-------------------

在面试中更多会考验开发人员对于指定场景的多线程知识,接下来就来看看一些综合运用

八、综合运用一

1.下列代码会报错吗?

int a = 0;while (a < 5) {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        a++;     });}
  • 编译会报错Variable is not assignable (missing __block type specifier)
    • 这块属于block的知识
  • 捕获外界变量并进行修改需要加__block int a = 0;
    • 这块内容在接下来的block会讲到

2.下列代码的输出

__block int a = 0;while (a < 5) {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        a++;    });}NSLog(@"%d", a);
  • 会输出0吗?
    • 不会,尽管是并发异步执行,但是有while在,不满足条件就不会跳出循环
  • 会输出1~4吗?
    • 不会(原因请往下看)
  • 会输出5吗?
    • 有可能(原因请往下看)
  • 会输出6~∞吗?
    • 极有可能

分析:

  • 刚进入while循环时,a=0,然后进行a++
  • 由于是异步并发会开辟子线程并有可能超车完成
    • 线程2a=0执行a++时,线程3有可能已经完成了a++使a=1
    • 由于是操作同一片内存空间,线程3修改了a导致线程2a的值也发生了变化
    • 慢一拍的线程2对已经是a=1进行a++操作
  • 同理还有线程4线程5线程n的存在
    • 可以这么理解,线程2、3、4、5、6同时在a=0时操作a
    • 线程2、3、4、5按顺序完成了操作,此时a=4
    • 然后线程6开始操作了,但是它还没执行完就跳到了下一次循环了开辟了线程7开始a++
    • 线程6执行结束修改a=5之后来到while条件判断就会跳出循环
    • 然而I/O输出比较耗时,此时线程7又刚好完成了再打印,就会输出大于5
  • 也有那么种理想情况,异步并发都比较听话,刚好在a=5时没有子线程
    • 此时就会输出5

如果还没有明白可以在while循环中添加打印代码

__block int a = 0;while (a < 5) {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        NSLog(@"%d————%@", a, [NSThread currentThread]);        a++;    });}NSLog(@"此时的%d", a);

打印信息证明while外面的打印已经执行,但是子线程还是有可能在对a进行操作的

3.怎么解决线程不安全?

可能有的小伙伴说这种需求不存在,但是我们只管解决便是了

此时我们应该能想到一下几种解决方案:

  • 同步函数替换异步函数
  • 使用栅栏函数
  • 使用信号量
  1. 同步函数替换异步函数
  • 结果:能满足需求
  • 效果:不是很好——能使用异步函数去使唤子线程为什么不用呢(虽然会消耗内存,但是效率高)
  1. 使用栅栏函数
  • 结果:能满足需求
  • 效果:一般
    • 首先栅栏函数全局队列搭配使用会无效,需要更换队列类型;
    • 其次dispatch_barrier_sync会阻塞线程,影响性能
    • dispatch_barrier_async不能满足需求,它只能控制前面的任务执行完毕再执行栅栏任务(控制任务执行)可是异步栅栏执行也是在子线程中,当a=4时会先继续下一次循环添加任务到队列中,再来异步执行栅栏任务(不能控制任务的添加)

__block int a = 0;dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);while (a < 5) {    dispatch_async(queue, ^{        a++;    });    dispatch_barrier_async(queue, ^{});}NSLog(@"此时的%d", a);sleep(1);NSLog(@"此时的%d", a);--------------------输出结果:-------------------此时的5此时的17--------------------输出结果:-------------------
  1. 使用信号量
  • 结果:能满足需求
  • 效果:很好、简洁效率高

__block int a = 0;dispatch_semaphore_t sem = dispatch_semaphore_create(0);while (a < 5) {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        a++;        dispatch_semaphore_signal(sem);    });    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);}NSLog(@"此时的%d", a);sleep(1);NSLog(@"此时的%d", a);--------------------输出结果:-------------------此时的5此时的5--------------------输出结果:-------------------

九、综合运用二

1.输出内容

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);NSMutableArray *marr = @[].mutableCopy;for (int i = 0; i < 1000; i++) {    dispatch_async(queue, ^{        [marr addObject:@(i)];    });}NSLog(@"%lu", marr.count);
  • 你:输出一个小于1000的数,因为for循环中是异步操作
  • 面试官:回去等消息吧
  • 然后你回去之后试了下大吃一惊——程序崩了

这是为什么呢?

其实跟综合运用一是一样的道理——for循环异步时无数条线程访问数组,造成了线程不安全

2.怎么解决线程不安全?

  • 使用串行队列

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);NSMutableArray *marr = @[].mutableCopy;for (int i = 0; i < 1000; i++) {    dispatch_async(queue, ^{        [marr addObject:@(i)];    });}NSLog(@"%lu", marr.count);--------------------输出结果:-------------------998--------------------输出结果:-------------------
  • 使用互斥锁

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);NSMutableArray *marr = @[].mutableCopy;for (int i = 0; i < 1000; i++) {    dispatch_async(queue, ^{        @synchronized (self) {            [marr addObject:@(i)];        }    });}NSLog(@"%lu", marr.count);--------------------输出结果:-------------------997--------------------输出结果:-------------------
  • 使用栅栏函数

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);NSMutableArray *marr = @[].mutableCopy;for (int i = 0; i < 1000; i++) {    dispatch_async(queue, ^{        [marr addObject:@(i)];    });    dispatch_barrier_async(queue, ^{});}NSLog(@"%lu", marr.count);

3.分析思路

单路千万条,跳跳通罗马——当然除了这三种还有其他办法

  • 使用串行队列
    • 虽然效率低,但总归能解决线程安全问题
    • 虽然串行异步是任务一个接一个执行,但那是队列中的任务才满足执行规律
    • 要想得到打印结果1000,可以在队列中执行
    • 总的来说,能满足需求但不是很有效
  • 使用互斥锁
    • @synchronized是个好东西,简单易用还有效,但也没有满足我们的需求
    • 在for循环外使用队列内同步/异步都不能得到100
    • 要么先sleep一秒——这样不可控的代码是不可取的的
    • 且在iOS的锁家族中@synchronized效率很低
  • 使用栅栏函数
    • 栅栏函数可以有效的控制任务的执行
    • 且与综合运用一不同,本题中是for循环
    • 至于怎么得到打印结果1000,只需要在同一队列中打印即可(栅栏函数的注意点)

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);NSMutableArray *marr = @[].mutableCopy;for (int i = 0; i < 1000; i++) {    dispatch_async(queue, ^{        [marr addObject:@(i)];    });    dispatch_barrier_async(queue, ^{});}dispatch_async(queue, ^{    NSLog(@"%lu", marr.count);});

写在后面

多线程在日常开发中占有不少份量,同时面试中也是必问模块。但只有基础知识是一成不变的,综合运用题稍有改动就是另外一种类型的知识考量了,而且也有多种解决方案

推荐文集

  • iOS面试题大全-附答案
  • 2021最新iOS面试题-BAT篇

推荐视频分析

  • iOS底层进阶面试合集:iOS底层+iOS逆向+Swift+大厂面试分享



©著作权归作者所有:来自51CTO博客作者wx5fb7717101de4的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. java中的锁介绍
  2. 大数据成神之路-Java高级特性增强(多线程)
  3. java技术分享:jvm对java的原生锁做了哪些优化?
  4. 面试官再问你优先级队列,请把这篇文章丢给他
  5. Java高并发编程基础三大利器之CountDownLatch
  6. 3-12(队列的结束以及树的开始)
  7. Java 并发编程 join 方法的使用
  8. JVM实战调优(空格引发的服务异常)
  9. 3-11(栈和队列的相互实现)

随机推荐

  1. Android Settings 设置中 PreferenceActi
  2. Android Activity 属性介绍
  3. android studio 的自动更新问题
  4. Android添加快捷方式(Shortcut)到手机桌
  5. Android之辅助服务下篇————Accessibi
  6. Android 设置EditText不会自动弹出键盘
  7. Android个人开发笔记
  8. Android studio 3.1.3 无法获取pom文件
  9. Android开发实现拖动效果
  10. Android(安卓)获取当前系统输入法和修改