美文网首页
函数与队列

函数与队列

作者: 深圳_你要的昵称 | 来源:发表于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