前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GCD原理探究(一)——创建队列

GCD原理探究(一)——创建队列

作者头像
拉维
发布2021-03-25 14:42:39
6820
发布2021-03-25 14:42:39
举报
文章被收录于专栏:iOS小生活iOS小生活

iOS内存的五大区

1,栈区,由编译器自动分配并释放,在运行的时候分配,用于存储函数的参数、局部变量、指针等。

2,堆区,由开发者分配和释放(如果开发者不释放,那么在程序结束的时候可能会由系统回收),在运行的时候分配,主要用于存储OC中使用alloc/new创建的对象,或者C语言中通过malloc、calloc、realloc分配的空间。

3,全局静态区,由系统分配和释放,在编译的时候分配,全局变量和静态变量都存在这里,初始化的全局或者静态变量存在一块区域,没有初始化的全局或者静态变量存储在相邻的另外一块区域。

4,文字常量区,由系统分配和释放,在编译的时候分配,存储字符串常量。

5,代码区,由系统分配和释放,在编译的时候分配,程序的代码被编译成二进制之后存在这里。

多线程的原理

多线程其实就是CPU在单位时间内快速在各个线程之间切换

GCD 的几个小题目

题目1

首先看个题目:

代码语言:javascript
复制
dispatch_queue_t queue = dispatch_queue_create("norman", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

伙伴们猜一下打印顺序。

先分析一下整个任务吧。

主队列中有三个任务:打印任务1(NSLog(@"1"))、异步添加到并发队列任务234

代码语言:javascript
复制
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});

)、打印任务5(NSLog(@"5"))

异步添加到并发队列任务234中异步添加的任务是在一条多线程中执行,它又有三个小任务在串行执行,这三个小任务分别是:打印任务2(NSLog(@"2"))、异步添加任务到并发队列任务3

代码语言:javascript
复制
    dispatch_async(queue, ^{
        NSLog(@"3");
    });

)、打印任务4(NSLog(@"4"))。

其中,异步添加任务到并发队列任务3中异步添加的任务是打印任务3打印任务3是在另外一条多线程中执行。

因此得出如下结论:

打印任务1首先执行,打印任务2打印任务5的执行先后顺序是不确定的,有可能2在5前,有可能5在2前,但是打印任务2打印任务5肯定是在打印任务1之后;

打印任务3打印任务4的执行先后顺序是不确定的,有可能3在4前,有可能4在3前,但是打印任务3打印任务4肯定是在打印任务2之后。

题目2

代码语言:javascript
复制
dispatch_queue_t queue = dispatch_queue_create("norman", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    NSLog(@"1");
});

dispatch_async(queue, ^{
    NSLog(@"2");
});

dispatch_sync(queue, ^{
    NSLog(@"3");
});

NSLog(@"4");

dispatch_async(queue, ^{
    NSLog(@"5");
});

dispatch_async(queue, ^{
    NSLog(@"6");
});

dispatch_async(queue, ^{
    NSLog(@"7");
});

主线程中执行的任务有7个:

  1. 异步添加打印任务1到并发队列
  2. 异步添加打印任务2到并发队列
  3. 同步添加打印任务3到并发队列
  4. 打印任务4
  5. 异步添加打印任务5到并发队列
  6. 异步添加打印任务6到并发队列
  7. 异步添加打印任务7到并发队列

其中,执行同步添加打印任务3到并发队列的时候 ,需要等待打印任务3执行完毕才能进行下一步的打印任务4

打印任务1打印任务2虽然是在另外两条多线程中执行,但是由于打印任务3的阻塞,实际上在打印任务4执行之前,打印任务1打印任务2打印任务3都会执行完毕。

打印任务4执行完毕之后,才会将打印任务5打印任务6打印任务7异步添加到并发队列,因此打印任务5打印任务6打印任务7的执行肯定是在打印任务4之后。

题目3

代码语言:javascript
复制
dispatch_queue_t queue = dispatch_queue_create("norman", NULL);

NSLog(@"1");
    
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
    
NSLog(@"5");

主线程中有三个任务:打印任务1异步添加任务到串行队列任务234打印任务5

异步添加任务到串行队列任务234中异步添加的任务是在另外一条多线程中执行的,其中有4个任务:打印任务2异步添加任务到串行队列任务3打印任务4打印任务3,由于是在串行队列中,所以它们依次排队挨个执行。

所以最后的打印结果是:

  1. 首先执行打印任务1
  2. 打印任务5打印任务2并发执行,其先后顺序不一定
  3. 打印任务2打印任务4打印任务3按顺序依次执行

题目4

在上面?题目3的基础上,仅做一个改动:添加打印任务3的时候由异步添加改为同步添加。

代码语言:javascript
复制
dispatch_queue_t queue = dispatch_queue_create("norman", NULL);

NSLog(@"1");
    
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
    
NSLog(@"5");

我们来分析一下。

主线程中有三个任务:打印任务1异步添加任务到串行队列任务234打印任务5

异步添加任务到串行队列任务234中异步添加的任务是在另外一条多线程中执行的,其中有4个任务:打印任务2同步添加任务到串行队列任务3打印任务4打印任务3,由于是在串行队列中,所以它们依次排队挨个执行。

这时,问题就来了,同步添加任务到串行队列任务3需要等待打印任务3执行完毕才能执行接下来的任务,而打印任务3又需要等同步添加任务到串行队列任务3执行完毕之后才能执行。这就是典型的死锁问题!

GCD中队列创建的源码分析

应用程序的加载——dyld动态链接器的工作流程中,我们知道在应用程序加载的时候会依次进行libsystem、libdispatch和libobjc的初始化。在前面的文章中,我们已经将libobjc这个库讲完了,接下来我们就来看libdispatch库的源码。

GCD的源码就是在libdispatch库里面。当然这是开天眼了,那么我们怎么去研究呢?

首先在创建队列的地方下个断点:

然后增加名为“dispatch_queue_create”的符号断点:

之后就可以看到,是在libdispatch.dylib库中了:

接下来我们就去获取libdispatch.dylib库的源码,然后开始分析。

我们就先来分析一下任务队列的创建吧,先找到dispatch_queue_create函数的实现。由于dispatch_queue_create函数的第一个参数是字符串,而C语言中的字符串定义是const char *,因此可以搜索关键字【dispatch_queue_create(const】:

然后我们就找到了函数的定义:

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
    dispatch_queue_t tq, bool legacy)
{
  
  dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);

  //
  // Step 1: Normalize arguments (qos, overcommit, tq)
  //

  dispatch_qos_t qos = dqai.dqai_qos;
#if !HAVE_PTHREAD_WORKQUEUE_QOS
  if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
    dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
  }
  if (qos == DISPATCH_QOS_MAINTENANCE) {
    dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
  }
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS

  _dispatch_queue_attr_overcommit_t overcommit = dqai.dqai_overcommit;
  if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) {
    if (tq->do_targetq) {
      DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
          "a non-global target queue");
    }
  }

  if (tq && dx_type(tq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE) {
    // Handle discrepancies between attr and target queue, attributes win
    if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
      if (tq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) {
        overcommit = _dispatch_queue_attr_overcommit_enabled;
      } else {
        overcommit = _dispatch_queue_attr_overcommit_disabled;
      }
    }
    if (qos == DISPATCH_QOS_UNSPECIFIED) {
      qos = _dispatch_priority_qos(tq->dq_priority);
    }
    tq = NULL;
  } else if (tq && !tq->do_targetq) {
    // target is a pthread or runloop root queue, setting QoS or overcommit
    // is disallowed
    if (overcommit != _dispatch_queue_attr_overcommit_unspecified) {
      DISPATCH_CLIENT_CRASH(tq, "Cannot specify an overcommit attribute "
          "and use this kind of target queue");
    }
  } else {
    if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
      // Serial queues default to overcommit!
      overcommit = dqai.dqai_concurrent ?
          _dispatch_queue_attr_overcommit_disabled :
          _dispatch_queue_attr_overcommit_enabled;
    }
  }
  if (!tq) {
    tq = _dispatch_get_root_queue(
        qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 4
        overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq; // 0 1
    if (unlikely(!tq)) {
      DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
    }
  }

  //
  // Step 2: Initialize the queue
  //

  if (legacy) {
    // if any of these attributes is specified, use non legacy classes
    if (dqai.dqai_inactive || dqai.dqai_autorelease_frequency) {
      legacy = false;
    }
  }

  const void *vtable;
  dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
  if (dqai.dqai_concurrent) {
    // 通过dqai.dqai_concurrent 来区分并发和串行
    // OS_dispatch_queue_concurrent_class
    vtable = DISPATCH_VTABLE(queue_concurrent);
  } else {
    vtable = DISPATCH_VTABLE(queue_serial);
  }
  switch (dqai.dqai_autorelease_frequency) {
  case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
    dqf |= DQF_AUTORELEASE_NEVER;
    break;
  case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
    dqf |= DQF_AUTORELEASE_ALWAYS;
    break;
  }
  if (label) {
    const char *tmp = _dispatch_strdup_if_mutable(label);
    if (tmp != label) {
      dqf |= DQF_LABEL_NEEDS_FREE;
      label = tmp;
    }
  }
    
  // 开辟内存 - 生成响应的对象 queue
  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;
  // 优先级
  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;
}

我们现在想研究的是它在底层是如何分别处理串行队列和并发队列的,而dispatch_queue_create函数的第二个参数就是用来控制串行队列和并发队列的,dispatch_queue_create函数的第二个参数对应的是_dispatch_lane_create_with_target函数的第二个参数dqa。

而_dispatch_lane_create_with_target函数的第二个参数dqa只在一个地方使用了,那就是传递给了_dispatch_queue_attr_to_info函数,如下:

那我们就下来就来看下_dispatch_queue_attr_to_info函数的源码:

代码语言:javascript
复制
dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
  dispatch_queue_attr_info_t dqai = { };

  if (!dqa) return dqai;

#if DISPATCH_VARIANT_STATIC
  if (dqa == &_dispatch_queue_attr_concurrent) {
    dqai.dqai_concurrent = true;
    return dqai;
  }
#endif

  if (dqa < _dispatch_queue_attrs ||
      dqa >= &_dispatch_queue_attrs[DISPATCH_QUEUE_ATTR_COUNT]) {
    DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
  }

  // 苹果的算法
  size_t idx = (size_t)(dqa - _dispatch_queue_attrs);

  // 位域
  // 0000 000000000 00000000000 0000 000  1
  
  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;

  dqai.dqai_relpri = -(idx % DISPATCH_QUEUE_ATTR_PRIO_COUNT);
  idx /= DISPATCH_QUEUE_ATTR_PRIO_COUNT;

  dqai.dqai_qos = idx % DISPATCH_QUEUE_ATTR_QOS_COUNT;
  idx /= DISPATCH_QUEUE_ATTR_QOS_COUNT;

  dqai.dqai_autorelease_frequency =
      idx % DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;
  idx /= DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;

  dqai.dqai_overcommit = idx % DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;
  idx /= DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;

  return dqai;
}

关于_dispatch_queue_attr_to_info函数的源码,有以下几段需要说明:

1,该函数的返回值类型是dispatch_queue_attr_info_t,而dispatch_queue_attr_info_t是一个结构体(一般而言,C语言中以_t结尾基本都是结构体),其里面是位域:

代码语言:javascript
复制
typedef struct dispatch_queue_attr_info_s {
  dispatch_qos_t dqai_qos : 8;
  int      dqai_relpri : 8;
  uint16_t dqai_overcommit:2;
  uint16_t dqai_autorelease_frequency:2;
  uint16_t dqai_concurrent:1;
  uint16_t dqai_inactive:1;
} dispatch_queue_attr_info_t;

其含义如下:

  • dqai_qos,优先级
  • dqai_relpri,
  • dqai_overcommit,过渡限制
  • dqai_autorelease_frequency,
  • dqai_concurrent,是否是并发队列
  • dqai_inactive,

2,_dispatch_queue_attr_to_info函数中首先会初始化一个空的dispatch_queue_attr_info_t类型的dqai,如果传入的参数dqa为空,那么就会直接返回空的dqai。这也是为什么外界在创建queue的时候传入NULL会创建串行队列的原因。

3,当传入的参数dqa不为空的时候,会根据dqa对dqai进行一系列位域赋值操作。

现在我们再回到_dispatch_lane_create_with_target函数的源码。前面我们了解了串行队列与并发队列的区别处理,接下来就找找看队列的创建代码:

使用_dispatch_object_alloc函数进行开辟内存,这里就有必要介绍一个联合体类型dispatch_object_t:

代码语言:javascript
复制
typedef union {
  struct _os_object_s *_os_obj;
  struct dispatch_object_s *_do;
  struct dispatch_queue_s *_dq;
  struct dispatch_queue_attr_s *_dqa;
  struct dispatch_group_s *_dg;
  struct dispatch_source_s *_ds;
  struct dispatch_mach_s *_dm;
  struct dispatch_mach_msg_s *_dmsg;
  struct dispatch_semaphore_s *_dsema;
  struct dispatch_data_s *_ddata;
  struct dispatch_io_s *_dchannel;
} dispatch_object_t DISPATCH_TRANSPARENT_UNION;

可以看到,dispatch_object_t这个联合体里面包含了很多类型(比如队列类型、信号量类型等),这些类型的变量都可以在dispatch里面直接使用。

使用_dispatch_queue_init函数进行对象的初始化。

接下来我在外界创建一个串行队列和一个并发队列,然后打印其信息,比较一下二者:

我发现通过NSLOG打印的信息很少,所以我换了个思路,使用lldb:

二者的width是不一样的,这个width实际上就是_dispatch_queue_init函数中传入的第三个参数:dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1,当串行的时候是1,也就是0X1,很容易理解;并发的时候是DISPATCH_QUEUE_WIDTH_MAX,我们查看DISPATCH_QUEUE_WIDTH_MAX的定义:

0X1000 - 2 = 0Xffe,所以lldb里面并发队列打印的width就是0xffe

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档