OC底层知识点之-多线程(二)GCD上篇

3,310 阅读11分钟

系列文章:OC底层原理系列OC基础知识系列

上面文章我们说了不少跟线程有关的知识点传送门。本文我们继续学习多线程,我们主要介绍多线程GCD。

GCD简介

  • GCD全称:Grand Central Dispatch
  • GCD是纯C语言,提供了非常多的强大函数
  • GCD是非常高效的多线程开发方式,它并不是Cocoa框架的一部分

GCD优势

  • 1.GCD 是苹果公司为多核的并⾏运算提出的解决⽅案
  • 2.GCD 会⾃动利⽤更多的CPU内核(⽐如双核、四核)
  • 3.GCD 会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)
  • 4.开发者只需要告诉 GCD 想要执⾏什么任务,不需要编写任何线程管理代码 【总结】:GCD就是将任务添加到队列,并且指定执行任务的函数。

GCD使用

在GCD使用中我们只需要做两件事:1.定义任务。2.将任务添加到队列中。所以GCD的核心就是dispatch队列和任务。

GCD队列

下面是GCD获取队列的集中方式:

  • 1.主线程队列:提交的任务将会在主线程完成
    • 可以通过dispatch_get_main_queue()来获得。
    • 主队列就是主线程,它是一个串行队列,在iOS中只有主线程才能拥有权限向渲染服务提交图层信息,完成图形显示工作。所以和UI相关操作,必须在主线程执行。
  • 2.全局并发队列(Clobal Queue):全局并发队列由整个进程共享,有高、中(默认)、低、后台四个优先级
  • 3.自定义队列
    • 并发队列:
      • 全局队列是并发队列
      • 通过dispatch_queue_create创建,第二个参数赋值为DISPATCH_QUEUE_CONCURRENT等
      • 不用等待上个任务是否完成,直接启用新的线程执行新的任务。
    • 串行队列:
      • 通过dispatch_queue_create创建,第二个参数赋值为DISPATCH_QUEUE_SERIAL或者NULL。
      • 串行队列在同一时间只能执行一个任务 整体如下图所示:

GCD任务

GCD任务就是操作意思,就是你在block块中的代码通过什么方式执行。执行任务有两种方式:同步和异步,两者主要区别是:是否等待队列的任务执行结束,以及是否具备开辟线程的能力。

同步执行(sync)

  • 1.同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 2.只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行(async)

  • 1.异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 2.可以在新的线程中执行任务,具备开启新线程的能力。 我们看下GCD的最基本的写法: 下面我们再将队列和任务搭配执行看看打印结果,准备代码
/**
 同步并发
 */
- (void)concurrentSyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步并发-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 异步并发
 */
- (void)concurrentAsyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"异步并发-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 串行异步
 */
- (void)serialAsyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"串行异步-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 串行同步
 */
- (void)serialSyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"串行同步-%d-%@",i,[NSThread currentThread]);
        });
    }
}

通过任务执行方式和不同队列组合,我们通过打印信息可以得出如下结论:

  • 1.任务执行方式是异步或者同步只能决定是否开辟新的线程。同步(不开辟线程),异步(开辟新的线程)
  • 2.队列是并行还是串行只能决定是否开辟多条线程。串行(只开辟一条线程),并行(开辟多条线程,开辟多条线程的能力只有在异步执行中发挥作用
  • 3.异步并行执行任务是乱序的

死锁

造成死锁的主要原因就是任务相互等待,看下面代码: 运行代码: 发现报错了,报错原因就是死锁。下面我们分析下为什么会死锁: 这个方法有3步操作:

  • 任务一:132行打印1任务,此部分在主线程。
  • 任务二:137行打印3任务
  • 任务三:134-136行通过同步任务向主线程插入打印2任务 我们知道主线程是同步任务,任务一和任务二是先加入主线程,任务三会排在任务一,二后面。但是任务三是通过同步任务加入的。这就会出现下面的情况,任务三需要等待主线程执行完任务一,二后才会执行。而同步任务的出现会让任务二等待任务三执行完成后才执行,这就造成了在主线程中任务三等待任务二完成执行,在同步任务里出现任务二等待任务三完成执行,这就造成了相互等待。出现死锁崩溃 如下图所示更容易解释:

GCD原理初探

上面我们说了GCD的任务和队列,并通过代码打印来说明了任务和队列的关系,线面我们就来看看GCD的底层实现

确定GCD研究源码位置

我们想要研究GCD,却发现不知从哪入手,代码点击进去之后就走不下去了。那么我们怎么知道线程这部分的源码在哪呢?我们要确定源码,我们知道dispatch_queue_create方法可以创建线程,那么我们打断点试试 运行代码 这时就可以确定线程的源码在libdispatch.dylib中。我们在苹果的官方文档上下载libdispatch.dylib源码。

dispatch_get_main_queue()初探

我们先看dispatch_get_main_queue()主线程 下图是对主线程的解释(捡主要的说一下):

  • 569-570行:主队列是用来在应用程序上下文中进行交互的主线程和主runloop。
  • 579-580行:主队列会被自动创建,而且会在main()函数之前创建

在main()函数前被调用,就是在dyld过程中进行的

dispatch_get_main_queue()再探

我们打印下主线程,来看看主线程是什么样的

	dispatch_queue_t serial = dispatch_queue_create("Lj", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t conque = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
    NSLog(@"%@-%@-%@-%@",serial,conque,mainQueue,globQueue);

运行打印 我们通过打印结果看到主线程变为了OS_dispatch_queue_main: com.apple.main-thread,说明在底层系统进行命名为com.apple.main-thread。我们在libdispatch源码里搜索com.apple.main-thread看看 我们发现这个main函数是个结构体对象,我们看到很重要的DQF_WIDTH(行为宽度,作为非常重要的标记)为1(1就是串行,单线程),dq_serialnum也为1。我们看到这个结构体是_dispatch_main_q,上面说了主线程创建时间很早,那么我们看看_dispatch_main_q什么时候被调用的。我们搜索_dispatch_main_q后发现有很多

共有8个文件,42个地方出现

下面那么我们应该怎么办?

libdispatch_init

多线程的调用最早是创建,我们在讲dyld的加载是提到过线程的加载:libdispatch_init OC底层原理之-dyld加载流程传从门,搜索libdispatch_init( 我们看到libdispatch_init方法很多,我们说主要方法,看下7759行代码,我们上面说的静态结构体_dispatch_main_q的do_targetq等于_dispatch_get_default_queue(true)。后面就是对_dispatch_main_q进行一系列的操作(7762行:设置当前的主队列,7763行:绑定到相应的线程)。下面我们查看下绑定过程:_dispatch_queue_set_bound_thread。 通过上图源码我们可以看到,绑定的底层实现是通过os_atomic_rmw_loop2o方法处理的,这部分实不在libdispatch源码中,后续有机会我们再研究。

总结

主线程下层是_dispatch_main_q的结构体它是在dyly加载中通过libdispatch_init方法进行创建,它是一个相当于串行队列的队列

dispatch_get_global_queue

我们点击去看下: dispatch_get_global_queue需要传入两个参数:identifier和flags,注释对这两个参数进行了说明:

  • identifier:服务质量(优先级)
  • flags:预留使用 因为存在优先级,就说明整个项目中可以有多个dispatch_get_global_queue,那么如何去设计它呢?我们可以想到通过集合去收集dispatch_get_global_queue,下面我们通过com.apple.root.default-qos来查找一下dispatch_get_global_queue全局队列。

上图发现都是通过_DISPATCH_ROOT_QUEUE_ENTRY方法去创建的。这里面有各种各样不同优先级的全局并发队列。

我们再查看当前的结构体为_dispatch_root_queues,它也是一个静态结构体。

队列如何创建,DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT区别

上面我们简单的讲了下dispatch_get_main_queue()和dispatch_get_global_queue,知道他们底层是静态结构体。下面我们主要讲下队列的创建,以及串行和并行的实现原理

dispatch_queue_create

看下底层实现

发现dispatch_queue_create是通过_dispatch_lane_create_with_target创建的,参数分别为label以及attr,后面的DISPATCH_TARGET_QUEUE_DEFAULT、true是默认值

我们搜索_dispatch_lane_create_with_target看下其内部实现 方法很长,我们怎么研究?我们只需要关注返回值第2809行就可以了。它返回的就是我们的线程。下面我们看下_dispatch_trace_queue_create方法

我们看到_dispatch_introspection_queue_create方法传入的是dq,先创建dqic(653行创建,654行将dqic的dqic_queue._dq赋值为dq)。659行又将dq的do_finalizer赋值为dqic。之后就返回了upcast(dq)的_dqu。

上面并没有我们想要的东西。我们回到_dispatch_lane_create_with_target方法,再看返回值return _dispatch_trace_queue_create(dq)._dq;这个方法返回的是_dq,上面我们知道_dispatch_introspection_queue_create返回的dq._dq中的dq是进行赋值,和传入的dq其实是同一个。我们只需要研究_dispatch_lane_create_with_target传入的dq就可以了。 此时dq被创建

我们看到init方法里我们看到dqai.dqai_concurrent的属性,这个属性对线程的影响 我们看到dqai.dqai_concurrent确定的值就是width,1172行DOF_WIDTH()就是该队列支持的线程数,1就是串行(单线程),>1就是并行(多线程),也就是如果dqai.dqai_concurrent为true就是多线程,否则为单线程

下面我们看下dqai的创建 传入的dqa就是我们外界传入的值,我们看下_dispatch_queue_attr_to_info实现 我们看到dqai.dqai_concurrent跟idx相关,而idx跟dqa相关。而dqa就是我们在创建线程是传入的值(DISPATCH_QUEUE_CONCURRENT或者DISPATCH_QUEUE_SERIAL) 这个截图是如果dqa==&_dispatch_queue_attr_concurrent就为true就是多线程。

我们再回到_dispatch_lane_create_with_target方法 如果是串行,vtable赋值传值为queue_concurrent,如果是并行vtable赋值传值为queue_serial,这样写是为了赋值,vtable也是个对象 通过上图我们可以知道:并行队列vtable为:OS_dispatch_queue_concurrent_class,而串行队列vtable为:OS_dispatch_queue_serial_class。而vtable对象应该为并行:OS_dispatch_queue_concurrent,串行:OS_dispatch_queue_serial 下面我们去打印并发和串行队列: 我们发现串行和并行打印的结果和上面推测的一致。

我们再回到_dispatch_lane_create_with_target方法,继续看_dispatch_object_alloc方法。 我们是onjc2所以会走_os_object_alloc_realized方法,上面我们已经知道vtable在串行和并行赋值不同,在_os_object_alloc_realized中vtable值就是cls,1509行:创建是将isa指针指向了cls也就是指向了vtable

总结

队列创建底层是_dispatch_lane_create_with_target创建,通过传入的值来确定是串行还是并行队列dispatch_queue_t也是个对象,也会通过alloc,init进行创建。在alloc中将isa指针指向并发还是串行通过init来确定DOF_WIDTH()等属性

dispatch_async

上面讲了队列的创建,下面我们看下异步任务的实现

  • dq:就是穿过进来的队列
  • work:就是传进来的任务 我们看下代码怎么操作的
  • 890行:创建dc
  • 896行:任务包装器,用来接收,保存block

2633-2638行:将work保存在dc的dc_ctxt中,其实这个判断是不走的,会走下面,我们看下_dispatch_continuation_init_f,重点关注ctxt(将work进行copy)func(将work进行调用)。注意:func在2642行执行了方法,也就是func执行完后会进行析构或者释放

此时我们看到上面参数对应的就是ctxt和f。方法将ctxt和f分别保存到dc的dc_ctxt和dc_func属性中

探究dispatch_async中work的执行

从上面知道work就是任务,我们探究下 此时的work就是打印123456,我们打断点,运行,然后bt一下

我们看到start_wqthread,_pthread_wqthread是在libsystem_pthread源码中,而libdispatch源码中走的第一个方法就是_dispatch_worker_thread2。我们搜索一下

_dispatch_worker_thread2

红框就是下面执行的代码 在6581-6588行的循环中执行了_dispatch_continuation_pop_inline方法

最后调用的是f(ctxt)方法,我们在上面讲dispatch_async的_dispatch_continuation_init_f方法说了,最后会将调用任务方法放在f中,将任务放在ctxt中,此处得到验证

最后会调用_dispatch_continuation_init方法中的_dispatch_call_block_and_release 这就是block任务执行的整个流程。

拓展

相关面试题

【面试题 - 1】异步函数+并行队列 下面打印结果是什么?

- (void)textDemo2{
    dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");
}

答案:1,5,2,4,3

解题:上面讲了,queue为并发队列,不会阻塞线程,所以1,5先执行。而并行队列里包含并行队列,所以他们任务互不影响。所以2,4先打印,最后为3。

代码修改

【修改1】:将并行队列 改成 串行队列,对结果没有任何影响,顺序仍然是 1 5 2 4 3

【修改2】:在任务5之前,休眠2s,即sleep(2),执行的顺序为:1 2 4 3 5,原因是因为I/O的打印,相比于休眠2s,复杂度更简单,所以异步block1 会先于任务5执行。当然如果主队列堵塞,会出现其他的执行顺序。

【修改3】:将打印 NSLog(@"3");的异步dispatch_async,改为同步dispatch_sync。执行顺序是:1,5,2,3,4,原因:将之前的异步改为同步够,会阻塞打印2的线程,导致只有打印3执行完后才能执行打印4。

【面试题 - 2】异步函数嵌套同步函数 + 串行队列(即同步队列)

- (void)textDemo2{
    // 同步队列
    dispatch_queue_t queue = dispatch_queue_create("Lj", NULL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");
}

答案:1,5,2崩溃

原因:queue是串行队列,1,5,2正常打印不再解释,执行完2后(2当前线程为串行),打印3任务通过同步任务插入到串行队列,放在打印4的后面(在执行2串行任务里,4的打印在3的前面),但是同步任务有需要先执行3在执行4,就造成相互等待,造成死锁。

【修改】:将打印4去掉呢?

  • 还是会死锁,因为任务3等待的是异步block执行完毕,而异步block等待任务3执行完成,还是会相互等待,造成死锁 【面试题 - 3】 异步函数 + 同步函数 + 并发队列
下面代码的执行顺序是什么?(答案是 AC)
- (void)interview04{
    //并发队列
    dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{ // 耗时
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    
    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
}
A: 1230789
B: 1237890
C: 3120798
D: 2137890

答案:AC

  • 1.任务1 和 任务2由于是异步函数+并发队列,会开启线程,所以没有固定顺序
  • 2.任务7、任务8、任务9同理,会开启线程,所以没有固定顺序
  • 3.任务3是同步函数+并发队列,同步函数会阻塞主线程,但是也只会阻塞0,所以,可以确定的是 0一定在3之后,在789之前 【面试题 - 4】下面代码中,队列的类型有几种?
//串行队列 - Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("Lj", NULL);
    
//并发队列 - Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
    
//主队列 - Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
//全局并发队列 - Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

答案:1.串行队列:serialQueue,mainQueue 2.并发队列:concurrentQueue,globalQueue

上面已经说过了,这里不再说了。