iOS源码解析:多线程

栏目: IOS · 发布时间: 7年前

内容简介:iOS开发中经常要使用到多线程,在面试的时候也是经常问到,比较常见的面试题有下面这些:GCD中有两个用来执行任务的函数:面试题一:以下代码在主线程中执行,是否会产生死锁?

iOS开发中经常要使用到多线程,在面试的时候也是经常问到,比较常见的面试题有下面这些:

  • iOS的多线程方案有哪几种?你更倾向于哪一种?

  • GCD的队列类型。

  • 说一下OperationQueue和GCD的区别,以及各自的优势。

  • 线程安全的处理手段有哪些?

  • OC你了解的锁有哪些?在此基础上进行二次提问“

    1.自旋和互斥对比

    2.使用以上锁需要注意哪些?

    3.用C/OC/C++,任选其一,实现自旋或互斥。

    下面就带着这些问题,来总结一下多线程的相关问题。

iOS中的常见多线程方案

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

GCD基础回顾

GCD中有两个用来执行任务的函数:

  • 用同步的方式执行任务

    即在当前线程中去做事情

dispatch_sync(dispatch_queue_t  _Nonnull queue, ^{})
  • 用异步的方式执行任务

    即另外开线程去做事情

dispatch_async(dispatch_queue_t  _Nonnull queue, ^{})
  • 并发队列:

    可以让多个任务同时执行(自动开启多个线程同时执行任务)

    并发功能只有在异步函数下才有效

  • 串行队列:

    让任务一个接着一个的执行(只会开启一条线程,一个任务执行完毕后,再执行下一个任务)

同步,异步,串行,并行

  • 同步和异步的主要影响:能不能开启新的线程

    同步:在当前线程中执行任务,不具备开启新线程的能力

    异步:在新的线程中执行任务。具备开启新线程的能力

  • 并发和和串行的主要影响:任务的执行方法

    并发:多个任务同时执行( 会开启多条线程 )

    串行:一个任务执行完毕后,再执行下一个任务( 只会开启一个线程 )

下面是各种队列的执行效果:

并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务
异步(async) 有开启新线程,并发执行任务 有开启新线程,串行执行任务 没有开启新线程,串行执行任务

GCD中死锁的问题

面试题一:以下代码在主线程中执行,是否会产生死锁?

- (void)viewDidLoad {
    [super viewDidLoad];

    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2");
    });

    NSLog(@"执行任务3");
}

我们运行代码,发现产生了崩溃,说明产生了死锁。下面来分析一下为什么会产生死锁:

我们知道, dispatch_sync() 是同步执行,不会开辟新线程,并且要 dispatch_sync() 的block执行完了才会继续往下执行,所以任务2是加入到了主队列中。 主队列是串行队列,,所以会串行执行加入其中的任务,等一个任务执行完了再执行另外一个,在任务2加入主队列之前,viewDidLoad这个大任务已经加入了主队列,所以任务2要等ViewDIdLoad执行完,才会执行任务2,也就是等到任务3执行完,才会执行任务2,但是由于任务3在任务2后面,所以要等到任务2执行完了,才执行任务3。这样就造成了任务2等任务3,任务3等任务2,造成死锁。

面试题二:以下代码是否会发生死锁:

- (void)viewDidLoad {
    [super viewDidLoad];

    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
    });

    //

    NSLog(@"执行任务3");
}

运行程序,发现程序并没有崩溃,并且产生打印:

2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 执行任务3
2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 执行任务2

为什么把同步改成异步,就不死锁了呢?我们分析一下, 由于队列是主队列,一定是把任务2加到主队列中,并且在此之前viewDidLoad的任务已经加入到了主队列中,所以要viewDidLoad执行完了才能执行任务2,由于dispatch_async()是异步执行,所以不用等到任务2执行完了再执行任务3,可以直接执行任务3,任务3执行完了,viewDidLoad也就执行完了,也就可以执行任务2了,所以打印的结果一定是先执行任务3再执行任务2。

面试题3:以下代码是否会发生死锁:

- (void)viewDidLoad {
    [super viewDidLoad];

    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{//1
        NSLog(@"执行任务2");

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

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

        NSLog(@"执行任务3");
}

运行代码,发现在dispatch_sync这里产生了崩溃,打印结果如下:

2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 执行任务3
2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 执行任务2

下面分析一下为什么会产生死锁:

dispatch_async是异步执行,所以先打印了 执行任务3 ,然后把1这个block加入了串行队列中,这时串行队列中有1这个block,然后又向串行队列中加入了任务3,任务3需要同步执行,所以任务3执行完了才会执行任务4,由于串行队列中1这个block排在任务3前面,所以要1这个block完成才会执行任务3,也就是要任务4执行完了才会执行任务3,而任务4又要等到任务3执行完成,这样互相等待,造成死锁。

总结

造成死锁的条件1是同步,2是往当前的串行队列中添加事件。

直接获取全局队列和手动创建队列的关系

看下面一段代码:

dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_queue_t queue4 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue5 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);

    NSLog(@"%p, %p, %p, %p, %p", queue1, queue2, queue3, queue4, queue5);

打印结果:

0x1062e2500, 0x1062e2500, 0x1062e2680, 0x600000146bf0, 0x600000146ca0

queue1,queue2,queue3是直接获取的全局队列,从打印结果可以看出,如果优先级相同,则获取的是同一个队列,如果优先级不同,则获取的是不同的队列。queue4,queue5是手动创建的队列,即便它们的identifier相同,但是仍然创建了不同的队列。

面试题

面试题一:问下列代码的打印结果:

 dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

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

    NSLog(@"2");
}

我们先看一下打印结果:

很奇怪,2压根就没有打印,也就是压根就没有执行test方法,这是为什么呢?

我们把

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

改成

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

看看打印结果:

说明这样是能成功执行test函数的,我们在runtime中找一下 - (id)performSelector:(SEL)aSelector withObject:(id)object; 的源码:

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

可以看到,就是简单的调用 objc_msgSend() 。但是在runtime的源码中却没有找到 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; 的实现。

我们再修改一下代码,让其直接在主线程中执行:

 NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");

打印结果:

那么就有理由猜测 NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3"); 的执行和线程有关。

[self performSelector:@selector(test) withObject:nil afterDelay:.0]; 这句代码的本质是往runloop中去添加一个NSTimer,由于主线程中有runloop,所以可以正常执行,但是在子线程中默认是没有启动runloop的,所以NSTimer也就没有办法成功执行。

我们可以启动子线程中的runloop试一下:

dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue1, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");

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

打印结果:

GNUstep

Foundation框架是不开源的,所以我们想看其中的源码是看不到的。GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然它不是苹果官方的完整实现,但是和官方的实现十分接近,区别不大,在学习的时候我们可以用来作为参考。

源码地址: http://www.gnustep.org/resources/downloads.php

我们下载了GNUstep的代码后,打开找到Foundation文件夹,在这个文件夹下找到 NSRunLoop.m 这个文件,在这个文件中找到 - (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds 这个方法的实现:

- (void) performSelector: (SEL)aSelector
          withObject: (id)argument
          afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop     *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer  *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
                         target: self
                       argument: argument
                          delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

GCD队列组

思考:如何用gcd实现以下功能:

异步并发执行任务1,任务2

等任务1,任务2都执行完毕后,再回到主线程执行任务3

我们可以用dispatch_group_t和dispatch_group_notify来完成这个功能:

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_async(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });

如果需要在任务1和任务2完成之后再完成任务3,任务4,可以这样:

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_async(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_notify(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_notify(group, queue, ^{

        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4-%@", [NSThread currentThread]);
        }
    });

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hit Refresh

Hit Refresh

Satya Nadella、Greg Shaw / HarperBusiness / 2017-9-26 / USD 20.37

Hit Refresh is about individual change, about the transformation happening inside of Microsoft and the technology that will soon impact all of our lives—the arrival of the most exciting and disruptive......一起来看看 《Hit Refresh》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具