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

本文主讲多线程相关面试问题:包括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、你都用过哪些锁?结合实际谈谈你是怎样使用的?
答: