iOS底层(十四)-GCD(一)

1,039 阅读8分钟

一、什么是GCD

GCD全称是 Grand Central Dispatch, 纯C语言编写的API,提供了非常多的强大的函数。

GCD的优势:

  1. CGD是苹果公司为多核的并行运算提出的解决方案
  2. CGD会自动利用更多的CPU内核
  3. GCD会自动管理线程的生命周期
  4. 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理的代码

二、GCD的使用

看一下最基本的使用,来写一个简单的异步队列GCD

dispatch_block_t block = ^{
  NSLog(@"hello world");  
};
dispatch_queue_t queue = dispatch_queue_create("com.test", NULL);
dispatch_async(queue, block);

有的朋友可能会讲在queue中应该写 DISPATCH_QUEUE_SERIAL , 但是我们点进去就知道了。 DISPATCH_QUEUE_SERIAL这个宏其实就是NULL, 为了日后的记忆方便,我个人通常都写为NULL。

一般来调用GCD都会分为异步调用和同步调用:

  1. 异步 dispatch_async :

    1. 不用等待当前语句执行完毕,就可以执行下一条语句。
    2. 它会开启线程执行block的任务
    3. 异步实际上就是多线程的代名词。
  2. 同步dispatch_sync :

    1. 必须等待当前语句执行完毕,才会执行下一条语句
    2. 不会开启线程
    3. 在当前执行block的任务

还有执行任务的串行与并发的方式:

  1. 串行队列: 遵循First in first out原则, 多个任务会按照队列的结构依次执行。
  2. 并发队列: 多个任务同时进行,会导致乱序效果。任务消耗的时间与优先级、任务复杂度等有关系。

2.1、队列和函数的搭配

2.1.1、同步串行队列

特性:

  1. 意味着不会开启线程,在当前线程执行任务
  2. 任务按照队列结构一个接着一个
  3. 会产生堵塞

代码示例:

dispatch_queue_t queue = dispatch_queue_create("com.test1", NULL);
dispatch_sync(queue, ^{
    NSLog(@"1");
});
NSLog(@"2");

此时因为线程堵塞必须等待打印1后才会打印2.

2.1.2、异步串行队列

特性:

  1. 开启一条新的线程
  2. 任务按照队列结构一个接着一个
  3. 不会产生堵塞

代码示例:

dispatch_queue_t queue = dispatch_queue_create("com.test2", NULL);
dispatch_async(queue, ^{
    NSLog(@"1");
});
NSLog(@"2");

当程序开辟线程后,并不会等待打印1, 而是直接进入下一步打印2. 当打印2完成后,GCD内部的一些配置工作等完成,进行打印1.

注意: 因为GCD的开启需要一点点时间,所以相对于系统直接打印2的时间要长一点,所以并不会先打印1再打印2

2.1.3、同步并发队列

特性:

  1. 不会开启线程,在当前线程完成
  2. 任务按照队列结构一个接着一个
  3. 会产生堵塞 代码示例:
dispatch_queue_t queue = dispatch_queue_create("com.test3", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
    NSLog(@"1");
});
NSLog(@"2");

因为是同步队列, 所以必然会造成堵塞。

2.1.3、异步并发队列

特性:

  1. 开启线程,在当前线程执行任务
  2. 任务异步执行,没有顺序,与CPU调度有关
  3. 不会产生堵塞

代码实现:

dispatch_queue_t queue = dispatch_queue_create("com.test4", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"1");
});
NSLog(@"2");

2.2、复合调用

看第一段示例代码:

dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

分析:

  1. 这是一个并发
  2. 首先会打印1
  3. 进入异步函数,此时不会堵塞,应该会继续向下执行,打印5
  4. 此时异步任务执行,打印2
  5. 异步任务里又有一个异步,此时会不会堵塞,继续向下,打印4
  6. 最后执行最里面的异步任务, 打印3

最终的打印结果就是 1 5 2 4 3

第二段代码:

dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_CONCURRENT);
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

分析:

  1. 首先是连续两个异步任务,紧接着就是一个同步任务。也就意味着 1 2 3 打印的顺序不是固定的。并且1 2 的任务复杂明显的很小,所以在 3 0 后面打印的几率几乎为0
  2. 同步任务后面是 0 , 也就是必须等3打印完才会打印0, 所以3一定在0之前打印
  3. 0后面是连续三个异步函数,也就是说必须等到0打印完后才会执行。 所以 0一定在 7 8 9之前打印
  4. 7 8 9 的顺序是无序的

综上分析,答案应该是AC

第三段代码:

dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

分析:

  1. 串行队列会堵塞
  2. 打印1,碰到异步, 打印5
  3. 打印2, 碰到同步任务,把同步任务放进队列任务。把打印3放进队列任务
  4. 在执行同步任务的时候,这个同步任务又将打印3放进任务队列, 此时这个同步任务必须要完成打印3才会继续向下执行。但是这个打印3又排在打印4任务的后面。会造成同步任务等待打印3执行才会执行打印4, 但是打印3又在等待打印4的执行。 这就形成一个很经典的问题: 死锁。

所以最终的打印结果应该是1 5 2

三、GCD的具体实现

还是打开熟悉的oc750源码。 进入 dispatch_queue_create() :

就会发现没有下文了。继续之前用到过的汇编方法:

来到这里也是没有下文了。 这个时候就可以对当前的 dispatch_queue_create 下一个符号断点:

原来它在 libdispatch.dylib 这个里面。

进入官网下载一份libdispatch :

根据我们调用 dispatch_queue_create(), 第一个参数为const char *, 所以在 libdispatch中搜索 dispatch_queue_create(const char, 找到方法实现:

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(const ,来到具体实现 :

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);
    //省略
}

3.1、线程的创建

首先我们来看一下是如何实现串行和并发的, 也就是说第二个参数就并发与串行的设置。这函数里第一行就是与dqa相关,所以我们的重点应该就是在 _dispatch_queue_attr_to_info 里面。

全局搜索 _dispatch_queue_attr_to_info(dis

dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
        //定义一个结构体
	dispatch_queue_attr_info_t dqai = { };
        //串行队列传递过来的是NULL,意味着返回过去的dqai是空的
	if (!dqa) return dqai;

    //....省略
	
    //....位域处理省略
	dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT);
	idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;
    //....省略

	return dqai;
}

在串行队列的情况下,返回的 dqai 是一个空的结构体。

下面对 dqai.dqai_concurrent 进行了处理,也就意味着当是并发队列的时候,dqai.dqai_concurrent 里面是有值的。

来到 _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)
{
	
    //...省略
    
	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 (dqai.dqai_concurrent) {
		// 通过dqai.dqai_concurrent 来区分并发和串行
		// OS_dispatch_queue_concurrent_class
		vtable = DISPATCH_VTABLE(queue_concurrent);
	} else {
		vtable = DISPATCH_VTABLE(queue_serial);
	}
	
    //...省略
    
	// 开辟内存 - 生成响应的对象 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;

}

这里做了判断, overcommit 记录是否是并发的开关, vtable 决定了是并发任务还是串行任务。

来写一个串行一个并发的打印出来看一下:

在构造方法哪里我们看到的:

dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1

这个width貌似很适合这里,那么再去看一下 DISPATCH_QUEUE_WIDTH_MAX:

0x1000 - 2 = 0xffe 表示并发队列需要记录这个with的最大值。

这个width不能满,起码要留一些空间。猜测这个东西很有可能是所谓的线程池。

在target中,有overcommit标记就是串行队列,没有overcommit就是并发队列。

在上面代码里,有一句 dq->do_targetq = tq; ,这个target是dq传递过来的。向上查到到一行tq相关代码:

#define DISPATCH_QOS_UNSPECIFIED        ((dispatch_qos_t)0)
#define DISPATCH_QOS_MAINTENANCE        ((dispatch_qos_t)1)
#define DISPATCH_QOS_BACKGROUND         ((dispatch_qos_t)2)
#define DISPATCH_QOS_UTILITY            ((dispatch_qos_t)3)
#define DISPATCH_QOS_DEFAULT            ((dispatch_qos_t)4)
#define DISPATCH_QOS_USER_INITIATED     ((dispatch_qos_t)5)
#define DISPATCH_QOS_USER_INTERACTIVE   ((dispatch_qos_t)6)
#define DISPATCH_QOS_MIN                DISPATCH_QOS_MAINTENANCE
#define DISPATCH_QOS_MAX                DISPATCH_QOS_USER_INTERACTIVE
#define DISPATCH_QOS_SATURATED          ((dispatch_qos_t)15)

tq = _dispatch_get_root_queue(
				qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
				overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
				

在创建GCD的时候,并没有添加qos相关的参数,那么qos在这个时候一定是空的, 也就是表示 _dispatch_get_root_queue 第一个参数正常情况下是 DISPATCH_QOS_DEFAULT 为 4, qos为4或者0,后面的 overcommit 判断是不是串行,结果应该为0或1

进入 _dispatch_get_root_queue

_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[]就有一种情况。 2 *(4 - 1) + 0或者1 = 6或7

来看一下 _dispatch_root_queues 的结构:

struct dispatch_queue_global_s _dispatch_root_queues[] = {

    //...省略
	
	_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,
	),
};

从上向下数,找到对应的第六个跟第七个, 里面的dq_label就是我们创建两个GCD所看到的 "com.apple.root.default-qos""com.apple.root.default-qos.overcommit"

从这里就知道,我们创建的GCD线程都应该是根据root_queue这个模板来进行构建的,除了mian_queue。

这些代码是从libdispatch 中看到的,也就是说当libdispatch 启动的时候就会加载这些东西,包括qos。程序的加载肯定会加载object相关了,所以可以从object_init()这个位置看一下。 在_os_object_init();下面有一个 _dispatch_introspection_init();

进入 _dispatch_introspection_init(); , 看到有一段

for (size_t i = 0; i < DISPATCH_ROOT_QUEUE_COUNT; i++) {
	_dispatch_trace_queue_create(&_dispatch_root_queues[i]);
}

表示在一个一个的去初始化,创建线程。

下面有一行 _dispatch_trace_queue_create(&_dispatch_main_q); 表示着主线程队列的创建。我们在创建一个dispatch_get_main_queue,这个函数的实现实际上就是查找 _dispatch_main_q。

我们除了用串行并发,还会使用到主队列与全局并发队列

用代码来打印一下主队列与全局并发队列:

先来看一下主队列,全局搜索一下 com.apple.main-thread:

这里面就是主队列的一些信息,可以看到标记为一个串行队列等。

在global里看到width = 0xfff。 0xffe给了我们自定义的并发队列,0xfff给了全局并发队列。