iOS底层-多线程之GCD(上)

584 阅读7分钟

前言

说到多线程,我们肯定就不会忽视GCD,因为它用法比较简洁,Api也比较易懂,对于处理多个任务等都是比较简单的,接来下将对GCD进行总结和探究。

简介

    1. GCD全称是Grand Central Dispatch,纯C语言Api,提供了非常多的强大函数
    1. GCD优势:
      1. GCD是苹果公司为多核的并行运算提出的解决方案
      1. GCD会自动利用更多的CPU内核(如:双核、四核)
      1. GCD自动管理线程的生命周期:创建线程调度任务销毁线程,程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

函数

  • 任务:GCD的任务使用block封装,block没有参数没有返回值 执行任务的函数:
  • 异步dispatch_async:不用等待当前语句执行完毕,就可以执行下一条语句
    • 会开启线程执行block的任务
    • 异步是多线程的代名词
  • 同步dispatch_sync:必须等待当前语句执行完毕,才会执行下一条语句
    • 不会开启线程
    • 在当前线程执行block任务

队列

队列分为串行队列并行队列,他们是一个数据结构,都遵循FIFO(先进先出)原则

串行队列

  • 串行队列在同一时间只能执行一个任务,如图所示: 截屏2021-08-08 18.03.50.png
  • 在根据FIFO原则先进先出,所以后面的任务必须等前面的任务执行完毕才能执行,就导致串行队列是顺序执行

并行队列

  • 并行队列是一次可以调度多个任务,但并不一定都能执行,线程的状态必须是runable时才能执行,所以先调度不一定先执行: 截屏2021-08-08 18.03.58.png
  • 如果可以看出,并行在同一时间能执行多个任务

案例分析

    1. 并发异步任务
- (void)textDemo1{
    dispatch_queue_t queue = dispatch_queue_create("wushuang.concurrent", DISPATCH_QUEUE_CONCURRENT); //并发队列
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
  • 执行顺序是什么呢,分析如下:

    • 首先1在主线程且顺序执行,所以1最新打印
    • 然后进入dispatch_async异步函数的block块,块里先执行2,同步函数会阻塞4的打印,所以快中3在2和4中间
    • 5是看异步函数执行的速度,有可能在1后面,也可能在234后面
  • 所以可能的结果是15234125341235412345

    1. 串行异步任务
- (void)textDemo1{
    dispatch_queue_t queue = dispatch_queue_create("wushuang.serial", NULL); //串行队列
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
  • 这个顺序回是怎样的呢,再来分析下
    • 首先1先执行
    • 然后执行dispatch_async块中的代码,块中的同步函数次时与第一种不一样,根据串行队列FIFO原则且是顺序执行,所以3执行的前提是dispatch_async函数执行完,但dispatch_async函数执行完的前提是块中dispatch_sync及之后代码能执行完,于是就出现了相互等待,造成了死锁

函数与关系

4种组合

函数与队列可以分为四种组合异步函数串行队列并发队列异步函数同步函数并发队列同步函数串行队列

    1. 异步函数串行队列开启线程,任务一个接着一个
    1. 异步函数并发队列开启线程,在当前线程执行任务,任务执行没有顺序,和cpu调度有关
    1. 同步函数并发队列不会开启线程,在当前线程执行任务,任务一个接着一个
    1. 同步函数串行队列不会开启线程,在当前线程执行任务,任务一个接着一个执行,会产生阻塞

主队列和全局队列

  • 主队列:专门在主线程上调度任务的串行队列不会开启线程,如果当前主线程正在执行任务,那么无论主队列中当前被添加了什么任务,都不会被调度dispatch_get_main_queue()
  • 全局队列:为了方便程序员的使用,苹果提供了全局队列dispatch_get_global_queue(0,0),全局队列是并发队列,在使用多线程时,如果对队列没有特殊要求,在执行异步任务时,可以直接使用全局队列
案例
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"会来吗?");
    });
}
  • ViewDidLoad中执行主线程同步任务,那么会打印吗?结果产生死锁
  • 分析:因为ViewDidLoad,在主线程上,主线程上调度任务的是主队列,主队列遵循FIFO原则,而要执行该同步任务也在主队列执行,所以必须等ViewDidLoad函数执行完才能执行,而ViewDidLoad函数执行完的前提是该同步任务执行完,所以就产生了相互等待,产生死锁

图解总结

截屏2021-08-08 22.17.19.png

队列源码分析

根据队列的介绍,我们知道有2种队列:串行并发,其中主队列是特殊的串行队列,全局队列是特殊的并发队列,那么在他们是怎么区分的呢?我们去打印堆栈看看:

截屏2021-08-09 16.16.42.png

主队列

在源码中搜索dispatch_get_main_queue

/*
 The main queue is meant to be used in application context to interact with the main thread and the main runloop.
 
 Returns the main queue. This queue is created automatically on behalf of the main thread before main() is called.
*/
dispatch_queue_main_t
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}
  • 根据注释可知:

    1. 主队列与程序的主线程runloop进行交互
    2. 主队列在main()之前程序自动创建的
  • 根据代码可知它的核心是调用了DISPATCH_GLOBAL_OBJECT函数,其中的有两个参数,第一个是类型,再来看看第二个参数_dispatch_main_q,搜索得到:

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,
};
  • 根据观察可发现队列的区分可能与dq_atomic_flagsdq_serialnum两个参数有关,值分别为DQF_THREAD_BOUND | DQF_WIDTH(1)1

全局队列

  • 再搜索dispatch_get_global_queue
dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);

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);
}
  • identifier可以设置一些优先级,有四种优先级:

    1. DISPATCH_QUEUE_PRIORITY_HIGH
    2. DISPATCH_QUEUE_PRIORITY_DEFAULT
    3. DISPATCH_QUEUE_PRIORITY_LOW
    4. DISPATCH_QUEUE_PRIORITY_BACKGROUND
  • flags是留给将来使用,任务非0值都可能导致NULL,通常传0

  • 这里返回的是_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函数: 截屏2021-08-09 16.53.42.png

此时找到了与dq_atomic_flags中相关参数DQF_WIDTH,传入的值为DISPATCH_QUEUE_WIDTH_POOL,也就是DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 1),但不能确定dq_serialnum,它的值跟label有关,再来打印下_dispatch_root_queueslabel

globalQueue: <OS_dispatch_queue_global: com.apple.root.default-qos>
  • 于是根据label在源码中确定dq_serialnum10,目前还是不能确定那个队列的区分和哪个参数有关

自定义队列

  • 再来搜索队列的创建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
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
  	dispatch_queue_t tq, bool legacy) // tq NULL,  legacy true
{
     dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa); // 面向对象封装
  
     ...

     dispatch_lane_t dq = _dispatch_object_alloc(vtable,
  		sizeof(struct dispatch_lane_s)); // 开辟内存
     _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
  		DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
  		(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // 初始化

     dq->dq_label = label; //label赋值
     dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
  		dqai.dqai_relpri);
     if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
         dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
     }
     if (!dqai.dqai_inactive) {
         dispatch_queue_priority_inherit_from_target(dq, tq);
   dispatch_lane_inherit_wlh_from_target(dq, tq);
     }
     _dispatch_retain(tq);
     dq->do_targetq = tq;
     _dispatch_object_debug(dq, "%s", __func__);
     return _dispatch_trace_queue_create(dq)._dq; // 创建痕迹标识,方便查找
}
  • 该函数第一个参数label我们比较熟悉,就是创建的线程的名字
  • _dispatch_queue_attr_to_info传入的第二参数:
dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
    dispatch_queue_attr_info_t dqai = { }; // 初始为NULL,
    if (!dqa) return dqai;

    ...
}
  • 这里先初始一个dispatch_queue_attr_info_t类型对象,然后在根据dpa类型进行相关赋值,如果dqa为不存在则直接返回,这就是串行可以传NULL的原因
  • 做好相关的准备工作后,接着在调用_dispatch_object_alloc方法对线程开辟内存
  • 再调用初始化函数_dispatch_queue_init,此处第三个参数有判断是否并判断,如果是并发传入为DISPATCH_QUEUE_WIDTH_MAX,串行则传入1,继续查看方法的实现:

截屏2021-08-09 17.32.24.png

  • 此处可以看出又出现了DQF_WIDTH()函数和dq_serialnum,并发DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 2),串行为DQF_WIDTH(1),但根据队列类型传入的参数只和DQF_WIDTH有关,那么dq_serialnum是什么呢?
  • 搜索_dispatch_queue_serial_numbers
unsigned long volatile _dispatch_queue_serial_numbers =
           DISPATCH_QUEUE_SERIAL_NUMBER_INIT;
  • 然后再搜索DISPATCH_QUEUE_SERIAL_NUMBER_INIT
// skip zero
// 1 - main_q
// 2 - mgr_q
// 3 - mgr_root_q
// 4,5,6,7,8,9,10,11,12,13,14,15 - global queues
// 17 - workloop_fallback_q
// we use 'xadd' on Intel, so the initial value == next assigned
#define DISPATCH_QUEUE_SERIAL_NUMBER_INIT 17
  • 根据注释得知1代表 main_14~15 代码 global queues,但能区分队列吗,还得看看os_atomic_inc_orig函数的实现:
#define os_atomic_inc_orig(p, m) \
     os_atomic_add_orig((p), 1, m)

#define os_atomic_add_orig(p, v, m) \
     os_atomic_c11_op_orig((p), (v), m, add, +)

#define _os_atomic_c11_op_orig(p, v, m, o, op) \
  	atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
  	memory_order_##m)
  • 最终得到C++方法atomic_fetch_add_explicit方法,网页搜索:

截屏2021-08-09 17.48.29.png

  • 原来是原子相关的操作,没啥用

总结:
1. 串行队列:DQF_WIDTH(1)
2. 全局队列:DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 1)
3. 创建的并发队列:DQF_WIDTH(DISPATCH_QUEUE_WIDTH_FULL - 2)

队列的继承

  • 在队列开辟内存时调用的是_dispatch_object_alloc方法,为什么不是_dispatch_dispatch_alloc?接下来根据队列的类型进行分析下

  • 先搜索队列的类型dispatch_queue_t

DISPATCH_DECL(dispatch_queue);
  • 再查看DISPATCH_DECL的实现:
#define DISPATCH_DECL(name) \
typedef struct name##_s : public dispatch_object_s {} *name##_t
  • 根据传入的参数dispatch_queue,得到:
struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t
  • 于是得到队列的继承关系:dispatch_queue_t : dispatch_queue_s : dispatch_object_s