美文网首页
23.iOS底层学习之GCD函数和队列

23.iOS底层学习之GCD函数和队列

作者: 牛牛大王奥利给 | 来源:发表于2022-01-07 00:27 被阅读0次

    本章提纲:
    1、GCD的介绍
    2、函数
    3、队列
    4、队列与函数的组合使用
    5、GCD部分源码解读
    6、GCD部分习题

    一、GCD的简介

    GCD的全称是:Grand Central Dispatch,是纯C语言实现的,是苹果公司为多核的并行运算提出的一套解决方案。

    GCD优点:
    • GCD会自动利用CPU内核。(比如双核,四核)
    • 自动管理线程的生命周期。(包括线程创建、调度、销毁)
    • 程序员只需要告诉GCD想要执行什么任务,不需要编写线程管理相关的代码,使用方便。
    二、函数

    在GCD中,执行任务的有两种方式,分别是:同步函数异步函数

    • 同步函数dispatch_sync
      1、必须等待当前语句执行完毕,才会执行下一条语句;
      2、不会开启新的线程;
      3、在当前线程中执行block的任务。

    • 异步函数dispatch_async
      1、不会等待当前语句执行完毕,就可以执行下一句;
      2、会开启线程执行block的任务,具备开启新线程的能力;
      3、异步是多线程的代名词。

    三、队列

    队列分为串行队列并发队列。队列是一种数据结构,遵循FIFO(Frist In Frist Out)先进先出的原则。后来的任务会排在队尾,先来的任务会被先执行。

    • 串行队列


      串行
    • 并行队列


      image.png

    在GCD中,有两个常见的队列,主队列(main)全局并发队列(global)。主队列属于串行队列,而全局队列是并发队列。

    • 主队列
      1、专门用来在主线程上调度任务的串行队列
      2、不会开启线程
      3、如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度。
      4、可以通过dispatch_get_main_queue来获得主队列。

    • 全局并发队列
      1、它是并发队列;
      2、在使用多线程开发时,如果没有特殊的要求,执行异步任务时,可以直接使用全局队列;
      3、使用dispatch_get_global_queue可以获得全局并发队列。

    全局队列和主队列的日常搭配使用示例:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //执行耗时操作,例如网络请求
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主线程进行UI操作,根据数据更新UI
        });
    });
    
    四、队列与函数的组合使用
    • 同步函数的串行队列
      1、不会开启新的线程,在当前线程执行任务
      2、任务串行的执行,一个接着一个的执行
      3、会产生阻塞

    • 同步函数的并发队列
      1、不会开启新的线程,在当前线程执行任务
      2、任务串行执行,一个接着一个的执行

    • 异步函数的串行队列
      1、开启一条新线程
      2、任务串行执行,一个接着一个执行

    • 异步函数的并发队列
      1、开启一条新线程
      2、任务异步执行,没有顺序,任务的顺序和CPU的调度有关

    小结:
    只有异步函数的并发队列的时候才会是真正的异步执行,没有顺序。
    同步函数或者串行队列都会导致任务最终串行执行,一个挨着一个的执行。

    五、GCD部分源码解读

    阅读GCD的源码还是相对比较困难的,这份源码的注释比较少,宏定义比较多,纯c语言实现。

    libdispatch-1271.120.2源码地址

    有了源代码,我们分别来了解一下我们常用的GCD函数底层实现。

    • dispatch_get_main_queue源码分析
    dispatch_queue_main_t
    dispatch_get_main_queue(void)
    {
        return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
    }
    

    通过上面的dispatch_get_main_queue的实现,我们可以看到,返回的是dispatch_queue_main_t类型。通过方法DISPATCH_GLOBAL_OBJECT来生成最后要返回的内容。而DISPATCH_GLOBAL_OBJECT的实现如下:

    #define DISPATCH_GLOBAL_OBJECT(type, object) ((OS_OBJECT_BRIDGE type)&(object))
    

    可以通过上面的代码看到DISPATCH_GLOBAL_OBJECT的最终结果是由typeobject两个参数进行与操作的结果。

    第一个参数传入的是dispatch_queue_main_t,这个通过查询源码发现是一个结构体dispatch_queue_static_s指针:

    typedef struct dispatch_queue_static_s *dispatch_queue_main_t;
    

    dispatch_queue_static_s的定义如下:

    // Cache aligned type for static queues (main queue, manager)
    struct dispatch_queue_static_s {
        struct dispatch_lane_s _as_dl[0]; \
        DISPATCH_LANE_CLASS_HEADER(lane);
    } DISPATCH_CACHELINE_ALIGN;
    

    根据这上面👆🏻的注释可以了解到,这是为了静态队列(主队列)服务的内存对齐类型。
    这是传入的第一个参数。
    第二个参数_dispatch_main_q经过搜索可以看到定义如下:

    // 6618342 Contact the team that owns the Instrument DTrace probe before
    //         renaming this symbol
    struct dispatch_queue_static_s _dispatch_main_q = {
        DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
    #if !DISPATCH_USE_RESOLVERS
        .do_targetq = _dispatch_get_default_queue(true),
    #endif
        .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
                DISPATCH_QUEUE_ROLE_BASE_ANON,
        .dq_label = "com.apple.main-thread",
        .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
        .dq_serialnum = 1,
    };
    
    • DISPATCH_GLOBAL_OBJECT_HEADER传递的参数是queue_main
    • dq_label貌似是队列名称
    • DQF_WIDTH是用来区分串行队列还是并发队列的,DQF_WIDTH(1)表示串行,并不是通过serialnum来表示串行和并行的。

    而且我们通过上面的源码关于dispatch_queue_static_s部分,最终返回dispatch_queue_main_t是通过dispatch_lane_s来完成的。

    (此部分比较困难,也是摸索着探究,很多地方也看不太懂,先整理一下初步的理解和思路,后续加深理解会更新)

    • dispatch_get_global_queue源码分析
    dispatch_queue_global_t
    dispatch_get_global_queue(intptr_t priority, uintptr_t flags)
    {
        dispatch_assert(countof(_dispatch_root_queues) ==
                DISPATCH_ROOT_QUEUE_COUNT);
    
        if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
            return DISPATCH_BAD_INPUT;
        }
        dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
    #if !HAVE_PTHREAD_WORKQUEUE_QOS
        if (qos == QOS_CLASS_MAINTENANCE) {
            qos = DISPATCH_QOS_BACKGROUND;
        } else if (qos == QOS_CLASS_USER_INTERACTIVE) {
            qos = DISPATCH_QOS_USER_INITIATED;
        }
    #endif
        if (qos == DISPATCH_QOS_UNSPECIFIED) {
            return DISPATCH_BAD_INPUT;
        }
        return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
    }
    
    • 方法dispatch_get_global_queue返回的类型是dispatch_queue_global_t
    • 最终调用的方法是_dispatch_get_root_queue,而_dispatch_get_root_queue的实现具体如下
    static inline dispatch_queue_global_t
    _dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
    {
        if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
            DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
        }
        return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
    }
    
    • 这个地方进行了优先级的判断

    而_dispatch_root_queues的实现如下:

    // 6618342 Contact the team that owns the Instrument DTrace probe before
    //         renaming this symbol
    struct dispatch_queue_global_s _dispatch_root_queues[] = {
    #define _DISPATCH_ROOT_QUEUE_IDX(n, flags) \
            ((flags & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) ? \
            DISPATCH_ROOT_QUEUE_IDX_##n##_QOS_OVERCOMMIT : \
            DISPATCH_ROOT_QUEUE_IDX_##n##_QOS)
    #define _DISPATCH_ROOT_QUEUE_ENTRY(n, flags, ...) \
        [_DISPATCH_ROOT_QUEUE_IDX(n, flags)] = { \
            DISPATCH_GLOBAL_OBJECT_HEADER(queue_global), \
            .dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE, \
            .do_ctxt = _dispatch_root_queue_ctxt(_DISPATCH_ROOT_QUEUE_IDX(n, flags)), \
            .dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL), \
            .dq_priority = flags | ((flags & DISPATCH_PRIORITY_FLAG_FALLBACK) ? \
                    _dispatch_priority_make_fallback(DISPATCH_QOS_##n) : \
                    _dispatch_priority_make(DISPATCH_QOS_##n, 0)), \
            __VA_ARGS__ \
        }
        _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
            .dq_label = "com.apple.root.maintenance-qos",
            .dq_serialnum = 4,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.maintenance-qos.overcommit",
            .dq_serialnum = 5,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
            .dq_label = "com.apple.root.background-qos",
            .dq_serialnum = 6,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.background-qos.overcommit",
            .dq_serialnum = 7,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
            .dq_label = "com.apple.root.utility-qos",
            .dq_serialnum = 8,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.utility-qos.overcommit",
            .dq_serialnum = 9,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
            .dq_label = "com.apple.root.default-qos",
            .dq_serialnum = 10,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
                DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.default-qos.overcommit",
            .dq_serialnum = 11,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
            .dq_label = "com.apple.root.user-initiated-qos",
            .dq_serialnum = 12,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.user-initiated-qos.overcommit",
            .dq_serialnum = 13,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
            .dq_label = "com.apple.root.user-interactive-qos",
            .dq_serialnum = 14,
        ),
        _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
            .dq_label = "com.apple.root.user-interactive-qos.overcommit",
            .dq_serialnum = 15,
        ),
    };
    
    • _dispatch_root_queues[]是个数组之类的东西,根据下标不同去哪对应的队列。
    • dq_serialnum这个貌似是表示不同类型的队列编号

    以上这两种常见的队列源码的实现大致如上。

    六、GCD习题解析
    • 主队列同步
    - (void)mainSyncTest{
        NSLog(@"0");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"1");
        });
        NSLog(@"2");
    }
    

    输出:0,然后死锁
    实际运行结果:

    image.png
    解析:默认是在主队列上,然后同步函数不会额外开启新的线程,也是在主队列上,主队列执行完0,然后准备往下接着执行遇见了自己队列上的同步函数,主队列顺序执行此时应该执行同步函数,但是同步函数又再主队列上 dispatch_sync(dispatch_get_main_queue()),又要接着等自己执行完毕,所以造成了死锁。
    也就是说主队列遇见同步函数没问题,但是同步函数又切回到了主队列上,主队列该执行同步函数了,然后同步函数切回主队列这样造成了死锁。
    • 主队列异步
    - (void)mainAsyncTest{
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"1");
        });
        NSLog(@"2");
    }
    

    输出:2,1
    解析:都在主队列上,异步函数不会阻塞,主队列先遇到异步函数,然后是2,然后是异步函数切回主队列上的1。所以打印是2,1

    • 全局队列同步
    /**
     全局同步
     全局队列:一个并发队列
     */
    - (void)globalSyncTest{
        for (int i = 0; i<20; i++) {
            dispatch_sync(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    

    输出:0~19顺序输出、然后hello queue
    解析:主队列遇见同步任务,等待同步任务内部执行完毕,因为是同步任务所以等待块返回才会执行下一个,所以结果是先顺序输出,然后输出hello queue

    • 全局队列异步
    /**
     全局异步
     全局队列:一个并发队列
     */
    - (void)globalAsyncTest{
        
        for (int i = 0; i<20; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    

    输出:hello queue、然后乱序
    解析:都在默认的主队列上,然后先遇到异步函数,然后是hello queue,所以先打印hello queue,然后异步函数执行内部的任务,是并发队列,异步任务,不用等待,所以后面乱序。

    • 函数与队列练习
      练习一
    - (void)textDemo{
        dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_async(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
    
        NSLog(@"5");
    }
    

    输出:1、5、2、4、3
    解析:主队列上是,1、异步块、5,所以先打印1,5;然后异步块中的内容是2、异步块、4;所以再打印2、4,然后最后一个异步块中是3,再打印3。结果是1、5、2、4、3。

    练习二

    - (void)textDemo1{  
        dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
        });
        NSLog(@"5");
    }
    

    输出:1、5、2,死锁
    实际运行:

    image.png
    解析:这个队列为NULL是默认是串行队列,主队列上首先是1、异步块、5,所以先打印1、5;然后异步块里面是一个串行队列上的一个2、同步块,2先打印,然后后面的同步块会阻塞队列后面的任务执行,同步块里的内容又等着同步函数执行完毕,同步函数等着队列上的3执行,又形成了死锁。

    练习三

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

    输出:1、5、2、3、4;
    解析:和上面的分析差不多,区别在与同步函数阻塞了以后因为是并发队列,不用等着同步函数了,就先执行了3,然后再4这样。

    • 微博面试题
    - (void)wbinterDemo{
        dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);
    
        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
    }
    

    答案:A,这个因为都是异步任务,但是内部都是在串行队列,1,2,3分别按照顺序入队到串行队列queue中,3的地方是同步要等前面的执行完,所以3执行完是0,后面的7,8,9和前面的一样顺序打印。所以出来的是1,2,3,0,7,8,9。

    • 美团面试题
    - (void)MTDemo{
        while (self.num < 5) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                self.num++;
            });
        }
        NSLog(@"end : %d",self.num); 
    }
    

    答案:大于等于5。因为while是死循环 至少是5以后才会跳出循环,而异步并发的时候self.num不一定被加过几次,反正小于5之前就一直在循环。

    • 快手面试题
    - (void)KSDemo{
        for (int i= 0; i<10000; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                self.num++;
            });
        }
        NSLog(@"end : %d",self.num); // 
    }
    

    答案:<=10000 ,异步并发队列执行到主队列上没顺序。

    相关文章

      网友评论

          本文标题:23.iOS底层学习之GCD函数和队列

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