美文网首页
函数与队列

函数与队列

作者: 深圳_你要的昵称 | 来源:发表于2020-11-04 20:40 被阅读0次

    前言

    函数与队列的一个具体结合的应用示例,就是苹果推出的GCD(Grand Central Dispatch)GCD是苹果公司为多核的并行运算提出的解决方案,它会自动利用更多的CPU内核(比如双核、四核),会自动管理线程的生命周期(创建线程、调度任务、销毁线程), 程序员只需要告诉 GCD想要执行什么任务,不需要编写任何线程管理代码。

    一、函数与队列的概念

    • 函数可以这么理解,就是调度任务,执行任务,说白可就是你想要做的事情。
      在GCD中任务就是👇
      • 任务使用block封装
      • 任务的 block 没有参数没有返回值
      • 任务分为两种类型:同步 & 异步
    1. 同步任务
      • 必须等待当前语句执行完毕,才会执行下一条语句
      • 不会开启线程
      • 在当前线程执行 block 的任务
    2. 异步任务
      • 不用等待当前语句执行完毕,就可以执行下一条语句
      • 会开启新线程执行 block 的任务
      • 异步是多线程的代名词
    • 队列是一种数据结构,遵循FIFO的原则,先进来先调度,注意是调度,而不是执行。

    函数与队列的关系示例👇

    - (void)syncTest{
        // 把任务添加到队列 --> 函数
        // 任务 --> dispatch_block_t ref,是个c对象
        dispatch_block_t block = ^{
            NSLog(@"hello GCD");
        };
        // 串行队列
        dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", NULL);
        // 函数
        dispatch_async(queue, block);
    }
    

    二、函数与队列的搭配

    大致有以下4种情况:

    1. 同步函数串行队列
      • 不会开启线程,在当前线程执行任务
      • 任务串行执行,任务一个接一个
      • 会产生堵塞
    2. 同步函数并发队列
      • 不会开启新的线程,在当前线程中执行任务
      • 任务一个接一个
    3. 异步函数串行队列
      • 开启一条新线程
      • 任务一个接一个
    4. 异步函数并发队列
      • 开启子线程,在当前线程中执行任务
      • 任务异步执行,没有顺序,与CPU调度有关

    三、相关的面试题剖析

    3.1 耗时问题案例

    - (void)wbinterDemo2 {
        // 耗能问题 - 同步和异步
        // 用 - 并发 多线程问题
        CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
        
        dispatch_queue_t queue = dispatch_queue_create("com.lgcooci.cn", DISPATCH_QUEUE_SERIAL);
       
    //    dispatch_async(queue, ^{
    
    //    });
    ////
        dispatch_sync(queue, ^{
    
        });        
        NSLog(@"%f",CFAbsoluteTimeGetCurrent()-time);
    }
    

    同步和异步都会耗时,那为什么还要使用,因为它们可以解决多线程同步和并发的一系列问题。

    3.2 并发队列案例

    - (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");
    }
    

    分析:

    • queue是一个并发队列
    • NSLog(@"1") 块dispatch_async(queue, ^{})NSLog(@"5")是依次添加到这个queue中,这3个任务遵循FIFO原则
    • 再看 块dispatch_async(queue, ^{}),这里执行任务有3个,NSLog(@"2") 块dispatch_async(queue, ^{ NSLog(@"3"); })NSLog(@"4"),所以该块任务比较耗时
    • 本身dispatch_async耗时,所以外层块任务执行顺序是 1> 5 > 子块,同理子块的执行顺序是 2 > 4 > 3,综合结论:1>5>2>4>3
      运行👇


    3.3 死锁案例

    稍微改下3.2的案例,将队列换成串行,同时将子块的异步换成同步

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

    运行👇


    直接报错,报错前运行结果是1 > 5 > 2,为什么会这样?
    分析:

    1. 出队列的情况如下👇
    1. 异步块块dispatch_async(queue, ^{})中有个同步块dispatch_async(queue, ^{ NSLog(@"3"); }),而同步块中的3是在异步块之后加入串行队列的,按照FIFO原则,异步块出队列之后,同步块才能出队列,但是同步块里必须等待当前语句NSLog(@"3");执行完毕,才会执行下一条语句NSLog(@"4");,然后异步块执行完毕,这样就形成了一个相互等待,异步块等待同步块dispatch_async(queue, ^{ NSLog(@"3"); }),同步块又继续等待异步块(队列的FIFO原则),如下图👇
    2. 这个相互等待的现象被称作死锁,死锁之前呢1,5和2已经出了队列,所以打印出来的就是1 > 5 > 2。
    3. 死锁问题,在堆栈中反应就是_dispatch_sync_f_slow

    3.4 同步阻塞案例

    - (void)wbinterDemo{
        dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
        // 1 2 3
        //  0 (7 8 9)
        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
    }
    

    这个案例比上面的死锁案例就简单很多了,根据同步块必须等待当前语句执行完毕,才会执行下一条语句这个原则,那么NSLog(@"0");肯定在NSLog(@"3");之后执行,同时NSLog(@"7"); NSLog(@"8");NSLog(@"9");都是异步块,执行顺序不固定,但是异步块都会有耗时,所以它们必须在NSLog(@"0");之后执行,所以3 > 0 > 7 8 9(无固定顺序),这样满足条件的选项只有AC

    运行看看👇

    四、队列的种类

    队列的种类就两种:串行队列 & 并发队列

    • 串行队列(Serial Dispatch Queue):
      一次只调度一个任务,队列中的任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),并且只会开启一条线程
    /**
     串行异步队列
    
     */
    - (void)serialAsyncTest{
        //1:创建串行队列
        dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
        for (int i = 0; i<20; i++) {
            dispatch_async(queue, ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        
        NSLog(@"hello queue");
    }
    
    /**
     串行同步队列 : FIFO: 先进先出
     */
    - (void)serialSyncTest{
        //1:创建串行队列
        dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
        for (int i = 0; i<20; i++) {
            dispatch_sync(queue, ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
    
    }
    

    运行👇


    串行队列异步任务
    串行队列同步任务
    • 并发队列(Concurrent Dispatch Queue):
      1. 多个任务同时执行,会开启多条线程执行
      2. 但是有个特例-->并发队列下执行同步任务,不会开启新的线程
    /**
     异步并发
     */
    - (void)concurrentAsyncTest{
        dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i<20; i++) {
            dispatch_async(queue, ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    
    /**
     同步并发
     */
    - (void)concurrentSyncTest{
        dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i<20; i++) {
            dispatch_sync(queue, ^{
                NSLog(@"%d-%@",i,[NSThread currentThread]);
            });
        }
        NSLog(@"hello queue");
    }
    

    运行👇


    并发队列异步任务

    上图可见,在并发队列执行异步任务时,会开启多条新的线程,且执行顺序我们无法控制,至于是哪条线程执行任务由队列决定,哪个任务先完成由CPU决定。例如结果中number = 6或number = 5等子线程执行了多次任务,那是因为这些子线程执行完任务就会被线程池回收,队列再从线程池中取线程执行任务,这时就会线程重复利用,详细流程可参考多线程里的线程池调度章节。

    并发队列同步任务

    并发队列下执行同步任务不会创建新线程,所有任务依次在主线程上执行。

    两种很常用的队列

    主队列 dispatch_get_main_queue
    全局队列 dispatch_get_global_queue

    4.1 dispatch_get_main_queue底层

    cmd+shitf+字母o搜索dispatch_get_main_queue

    查看声明的注释,可知2个重要点:

    • 主队列依赖当前的主线程和其RunLoop
    • 主队列的指向在main()方法调用之前自动创建的

    之前我们分析类的加载的流程可知,在main()调用之前,要么是dyld的调起动态库去调用main方法,要么是objc_init中的readImage回调中去触发main方法。其中dyld会加载libdispatch.dylib这个库。如何验证呢?--> 符号断点

    通过符号断点dispatch_get_main_queue,打dispatch_sync

    可见,查到GCD底层是libdispatch.dylib这个动态库。

    然后去官网下载源码,搜索_dispatch_main_q👇

    _dispatch_main_q 的类型是dispatch_queue_static_s,一个静态结构体,我们自定义队列的时候,需要指定队列的name,上图可知,主队列有个dq_labelcom.apple.main-thread,这个是不是主队列的name呢?我们可以lldb打印看看👇

    果然,com.apple.main-thread这就是主队列的name,dq_label是name字段的key值。

    那么问题来了,主队列的类型是串行还是并发?

    我们全局搜索一下_dispatch_main_q,看看在哪里初始化的?

    我们找到了libdispatch_init(void)


    框里的两句代码,将主队列设置为当前的默认队列,再看_dispatch_queue_set_bound_thread源码👇
    DISPATCH_ALWAYS_INLINE
    static inline void
    _dispatch_queue_set_bound_thread(dispatch_queue_class_t dqu)
    {
        // Tag thread-bound queues with the owning thread
        dispatch_assert(_dispatch_queue_is_thread_bound(dqu));
        uint64_t old_state, new_state;
        os_atomic_rmw_loop2o(dqu._dq, dq_state, old_state, new_state, relaxed, {
            new_state = old_state;
            new_state &= ~DISPATCH_QUEUE_DRAIN_OWNER_MASK;
            new_state |= _dispatch_lock_value_for_self();
        });
    }
    

    大致是即将主队列与主线程进行绑定。

    目前为止,没有发现主队列是串行或并发的相关代码。暂且我们先看看全局队列的源码,找找有没有相关的联系。

    4.2 dispatch_get_global_queue底层

    dispatch_queue_global_t
    dispatch_get_global_queue(long priority, unsigned long 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_queue_global_t类型,再搜索dispatch_queue_global_t👇

    是结构体dispatch_queue_global_s的指针。
    接着我们再来反向操作,根据线程name名称搜索👇


    搜索com.apple.root.default-qos👇


    再次验证是结构体dispatch_queue_global_s,属于一个集合_dispatch_root_queues[]的一个元素。

    再对比看看 _dispatch_main_qdispatch_queue_global_s


    DISPATCH_QUEUE_WIDTH_POOL宏定义👇

    此时,我们知道了dq_atomic_flags(可以理解为队列宽度),主队列是1,而全局队列是0x1000 - 1 -->转换成10进制就是4096-1 = 4095。大致可以看出,主队列应该是串行队列,而全局队列是并发的

    五、队列queue的底层

    如何验证主队列是串行,全局队列是并发?这还要从队列的底层源码看起,我们先看看队列的创建。

    5.1 队列queue的创建

    dispatch_queue_create源码👇

    dispatch_queue_t
    dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
    {
        return _dispatch_lane_create_with_target(label, attr,
                DISPATCH_TARGET_QUEUE_DEFAULT, true);
    }
    

    再看_dispatch_lane_create_with_target


    根据返回值_dispatch_trace_queue_create(dq)._dq;,再来看看_dispatch_trace_queue_create👇


    再回头来看 _dispatch_trace_queue_create(dq)._dq,其实_dispatch_trace_queue_create(dq)就是将dq里的某些字段完成了一系列的初始化,最后取字段_dq返回,重点就来到 -->dq的创建过程,然后它的字段_dq在哪里赋值

    dq创建相关的代码👇


    继续,看看初始化_dispatch_queue_init👇
    很明显,和_dispatch_trace_queue_create(dq)如出一辙,dqu的初始化依赖dqf widthinitial_state_bits。再来看这3个入参的来源。
    1. 先看dqf


      来到dqai的创建👇

      最终定位到dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
    2. widthinitial_state_bits
    _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
    

    根据上述源码,其实widthinitial_state_bits的值的来源-->也是dqai.dqai_concurrentdqai.dqai_inactive👇

        size_t idx = (size_t)(dqa - _dispatch_queue_attrs);
        
        dqai.dqai_inactive = (idx % DISPATCH_QUEUE_ATTR_INACTIVE_COUNT);
        idx /= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT;
    
        dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT);
        idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;
    

    其中宏定义👇

    #define DISPATCH_QUEUE_ATTR_INACTIVE_COUNT 2
    
    #define DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT 2
    

    到此,我们研究的重点最终来到了dqai

    5.2 串行队列和并发队列的区别

    还是按照溯源的原则,dqai的创建👇

    // dqai 创建 -
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
    

    dqa是方法入参👇

    那么就是attr👇

    // 并发
    dispatch_queue_t conque = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    
    // 串行
    dispatch_queue_t serial = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    

    不就是入参DISPATCH_QUEUE_CONCURRENT or DISPATCH_QUEUE_SERIAL么。

    再回头看看主队列全局队列,他们分别是什么类型的👇

    主队列

    上面我们分析过,主队列dispatch_get_main_queue对应的底层结构体是_dispatch_main_q👇


    然后看队列的初始化中👇,

    dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1这个三元表达式对应的参数是uint16_t width 👇

    width经过处理,赋值给了dq->dq_state。回头看主队列的dq_state👇

    都有一个DISPATCH_QUEUE_STATE_INIT_VALUE,并且主队列DISPATCH_QUEUE_STATE_INIT_VALUE(1)表示width是1,根据三元表达式dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1,串行队列才是1,所以主队列是串行队列

    全局队列

    上面分析,全局队列dispatch_get_global_queue对应的底层结构是dispatch_queue_global_s👇

    其中dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE定义👇

    #define DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE \
            (DISPATCH_QUEUE_WIDTH_FULL_BIT | DISPATCH_QUEUE_IN_BARRIER)
    
    #define DISPATCH_QUEUE_WIDTH_FULL_BIT       0x0020000000000000ull
    #define DISPATCH_QUEUE_IN_BARRIER           0x0040000000000000ull
    

    同理,根据dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1,那么并发队列的话widthDISPATCH_QUEUE_WIDTH_MAX,其定义👇

    #define DISPATCH_QUEUE_WIDTH_FULL           0x1000ull
    #define DISPATCH_QUEUE_WIDTH_MAX  (DISPATCH_QUEUE_WIDTH_FULL - 2)
    

    至此,我们知道并发队列widthDISPATCH_QUEUE_WIDTH_FULL - 2,必定大于1
    全局队列.dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE 肯定不等于DISPATCH_QUEUE_STATE_INIT_VALUE(1),那么全局队列是并发队列

    综上所述,我们得出结论👇

    主队列是串行队列,全局队列是并发队列。

    相关文章

      网友评论

          本文标题:函数与队列

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