iOS探索 多线程面试题分析

欢迎阅读iOS探索系列(按序阅读食用效果更加)程序员

写在前面

前面四篇文章分别介绍了多线程原理GCD的应用GCD底层原理NSOperation,本文将分析iOS面试中高频的多线程面试题,但愿各位看官都能答对(部份内容跟前几篇文章有点重复)面试

1、多线程的选择方案

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

注意:若是使用NSThread的performSelector:withObject:afterDelay:时须要添加到当前线程的runloop中,由于在内部会建立一个NSTimer数组

2、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须要咱们本身去管理线程的生命周期,还要考虑线程同步、加锁问题,形成一些性能上的开销

3、多线程的应用场景

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

4、线程池的原理

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

饱和策略有以下四个:数据结构

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

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

栅栏函数两个API的异同多线程

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

栅栏函数注意点并发

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

6、栅栏函数的读写锁

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

- (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知足:等队列中前面的读写任务都执行完了再来执行当前任务

7、GCD的并发量

不一样于NSOperation中能够经过maxConcurrentOperationCount去控制并发数,GCD须要经过信号量才能达到效果async

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)}
--------------------输出结果:-------------------
复制代码

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

8、综合运用一

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的存在
    • 能够这么理解,线程二、三、四、五、6同时在a=0时操做a
    • 线程二、三、四、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
--------------------输出结果:-------------------
复制代码

9、综合运用二

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);
});
复制代码

写在后面

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

相关文章
相关标签/搜索