美文网首页iOS面试
[iOS面试]第6章 多线程相关面试问题

[iOS面试]第6章 多线程相关面试问题

作者: codeTao | 来源:发表于2020-05-11 11:57 被阅读0次

    本文主讲多线程相关面试问题:包括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启动流程

    问题: 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、你都用过哪些锁?结合实际谈谈你是怎样使用的?
    答:

    相关文章

      网友评论

        本文标题:[iOS面试]第6章 多线程相关面试问题

        本文链接:https://www.haomeiwen.com/subject/rwxunhtx.html