面试遇到多线程的第一天-死锁

2,983 阅读7分钟

接着前面RunLoop的应用中,分享了关于线程保活的知识点,本文再继续由浅入深的写一写多线程的东西。
多线程也是面试中被问到概率很大的知识点,也是我们作为一名iOS开发者必须掌握的内容。本文我想以问答的形式,分享记录我对多线程的理解。

常见的多线程方案?

  • pthread: 一套通用的C语言API,适用于Unix、Linux、Windows等系统,可跨平台,可移植,但是使用难度大
  • NSThread:在RunLoop的应用中的示例代码中使用过,简单易用,可以直接操作线程对象
  • GCD:旨在替代NSThread等线程技术,也是C语言的API
  • NSOperation: 基于GCD封装的一套OC语言的API

下面对于常见的多线程方法,总结了一个对比的表格,帮助我们加深记忆:

上述几种常见的多线程方案,除pthread之外,其他几种底层都是基于pthread实现的。

根据上面的表格,以及平时工作的经验,我们也可以知道,开发中最常使用的是GCD和NSOperation这两种多线程方案,因为他们都使用简单,而且不需要程序员管理线程的生命周期。

GCD

先说一说GCD中常用的函数,GCD中有两个用来执行任务的函数,dispatch_sync是用同步的方式执行任务,同步,就是在当前线程中执行任务

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

dispatch_async用异步的方式执行任务,异步,就是可能会创建新线程并且可以在新线程中执行任务

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

其中queue是队列,block中就是要执行的任务。

队列的类型

队列可以分为串行和并发队列两大类,队列的类型决定了任务的执行方式,或者说是执行顺序,是并发或者串行执行。

其实还有主队列,主队列是一种特殊的串行队列。

再做一下名词解释吧,所谓并发,就是多个任务同时执行;而串行呢,是要一个任务执行完,再执行下一个任务。

是否开启新线程

dispatch_sync 和 dispatch_async , 决定了是否需要开启新的线程,dispatch_sync不需要开启新线程,dispatch_async则需要开启新线程,但是也不是绝对,比如使用了主队列。

队列的类型以及同步和异步,共同决定了在执行任务的时候,是否开启新线程,以及执行任务的方式。

总结一下:同步都是在当前线程(不需要开启新线程)中串行执行任务的,异步在主队列这个特殊队列中没有开启新线程,其他队列都会开启新线程,但是执行方式,由所在队列决定。

死锁

关于死锁的问题,但凡面试被问到多线程,十有八九要继续说一个场景,或者给一段代码,让你判断是否会发生死锁,以及发生死锁的原因。

其实产生死锁的原因可以总结为:调用dispatch_sync(同步的)往当前串行队列中添加任务,就会产生死锁。同步和串行队列的特点决定了会产生死锁。

下面举几个例子,每一段代码(默认在主线程调用),都分析一下是否会产生死锁:

- (void)interview01 {
        NSLog(@"执行任务1");
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            NSLog(@"执行任务2");
        });
        
        NSLog(@"执行任务3");
}

以上代码在主线程执行,会产生死锁嘛? 会
队列的特点:FIFO 先进先出, 本来主队列里面就有一个任务viewDidLoad 需要等待viewDidLoad执行完才能执行下一个任务
dispatch_sync 需要立马在当前线程执行任务, 执行完才能往下执行
总结:想要执行任务2 , 必须要等任务3(viewDidLoad)执行完, 而想要任务3执行完 又必须要任务2(dispatch_sync)执行完,所以产生死锁。

继续看下一个例子:

- (void)interview02 {
        NSLog(@"执行任务1");
        dispatch_queue_t queue = dispatch_get_main_queue();
    //  dispatch_async 不要求立马在当前线程执行任务 可以等上一个任务(interview02)执行完再继续执行
        dispatch_async(queue, ^{
            NSLog(@"执行任务2");
        });
        
        NSLog(@"执行任务3");
}

以上代码在主线程执行,会产生死锁嘛? 不会
dispatch_async 不要求立马在当前线程执行任务 可以等上一个任务(interview02)执行完再继续执行

继续看下一个例子:

- (void)interview03 {
    NSLog(@"执行任务1");
//    串行队列
    dispatch_queue_t queue = dispatch_queue_create(@"myqueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
        dispatch_sync(queue, ^{
            NSLog(@"执行任务3");
        });
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}

以上代码在主线程执行,会产生死锁嘛? 会
根据文章上一节贴的那张关于同步/异步和队列的表格,可知在手动创建的串行队列中,调用dispatch_async虽然开启了新线程,但也是串行执行,再通过dispatch_sync添加同步任务,依然会造成死锁。

对于interview03中的死锁,可以有两种解决方式:

  1. 任务3放在并发队列中
  2. 任务3放在其他的串行队列中
- (void)interview03 {
    
    NSLog(@"执行任务1");
//    串行队列
    dispatch_queue_t queue = dispatch_queue_create(@"myqueue", DISPATCH_QUEUE_SERIAL);
 //  并发队列
    dispatch_queue_t queue2 = dispatch_queue_create(@"myqueue2", DISPATCH_QUEUE_CONCURRENT);
 //  另外一个串行队列
    dispatch_queue_t queue3 = dispatch_queue_create(@"myqueue3", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
//        dispatch_sync(queue, ^{
//            NSLog(@"执行任务3");
//        });
//        换成并发队列 就不会产生死锁  放在不同的队列中  就不存在互相等待的问题
        dispatch_sync(queue2, ^{
            NSLog(@"执行任务3");
        });
        
//        换一个串行队列 也不会产生死锁
        dispatch_sync(queue3, ^{
                   NSLog(@"执行任务3");
               });
        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}

继续看下一个例子:

- (void)interview04 {
    NSLog(@"执行任务1");
//    并发队列 
    dispatch_queue_t queue = dispatch_queue_create(@"myqueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");

        dispatch_sync(queue, ^{
            NSLog(@"执行任务3");
        });
        

        NSLog(@"执行任务4");
    });
    
    NSLog(@"执行任务5");
}

以上代码在主线程执行,会产生死锁嘛?不会
并发队列 不会阻塞

根据对上面几个例子的分析,是不是也验证了,产生死锁的原因。再复习一遍产生死锁的原因总结为:调用dispatch_sync(同步的)往当前串行队列中添加任务,就会产生死锁。

RunLoop和线程

这个知识点算是复习吧,前面RunLoop的应用里说过的内容,这里换一种形式被问到,可能会有点儿迷糊,直接看下面这段代码

- (void)test {
    NSLog(@"2");
}

- (void)interview05 {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);  
    dispatch_async(queue, ^{
        NSLog(@"1");
		[self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
}

这个问题是问最终的打印结果,test方法中的2是打印不出来的,结果只打印了1 和 3.

原因是performSelector: withObject:afterDelay:这个方法,他并不能等价于[self test]这种调用方式, 这个方法是定义在NSRunLoop.h文件中的,底层实现是设置了一个定时器,然后需要把定时器添加到当前线程的runloop中,RunLoop的应用中说过,子线程默认是不会自动创建runloop的,所以这里通过performSelector: withObject:afterDelay:这个方法调用test 是无效的。

替换成下面这句代码,就可以打印出 1 2 3 ,因为他是等价于[self test]的

[self performSelector:@selector(test) withObject:nil];

这两个方法的申明很像,就差了最后一个参数,但是performSelector:withObject:是定义在NSObject中的。

还有一种方案,那就是使用线程保活,也可以让test方法执行,添加下面两句代码到block中

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

再继续分析,如果是放在主线程中又会是什么打印结果呢?

    NSLog(@"1");
//    往主runloop中添加了定时器
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
    NSLog(@"3");

测试之后发现,打印结果是 1 3 2,这里就要根据之前分享过的RunLoop的原理中说过的处理逻辑来分析了,虽然delay是0,也是要等到runloop来处理定时器,才能执行。

写完了这篇分享的文章,对于多线程和runloop,以及多线程和runloop之间的关系理解的也更加深刻了。学习就是要这样一步步来,然后多复习,温故真的能知新。