美文网首页
OC底层知识点之-多线程(二)GCD上篇

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

作者: iOS_子矜 | 来源:发表于2021-08-14 22:18 被阅读0次

    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崩溃 [图片上传失败...(image-338b41-1628941505662)]

    原因: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

    收录

    相关文章

      网友评论

          本文标题:OC底层知识点之-多线程(二)GCD上篇

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