iOS底层学习 - 多线程之GCD队列原理篇

3,311 阅读7分钟

经过前两章的学习,我们对多线程和GCD的使用已经有了了解,但是对于底层原理并不熟悉,我们知道GCD使用主要有队列和函数两个参数,我们就来一一探究,本章节我们先来看一下GCD的队列是如何创建的

系列文章传送门:

iOS底层学习 - 多线程之基础原理篇

iOS底层学习 - 多线程之GCD初探

简介

我们已经知道了,GCD中的队列(FIFO)主要有以下四种:

我们可以通过dispatch_queue_create方法来生成一个dispatch_queue_t对象供GCD来使用,那么在底层队列是如何创建的呢?

创建自定义队列dispatch_queue_create

要像知道是底层是如何创建的,最好的方法还是阅读源码,万幸的是,多线程的代码,苹果是开源的,可以点击libdispatch源码下载。

在底层源码中,我们可以看到dispatch_queue_create的底层实现如下,可以看到调用了一个中间代码方法。下面我们深入_dispatch_lane_create_with_target方法,来看一下究竟。

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_queue_attr_info_t

其中参数dispatch_queue_attr_t为传入的串行还是并行队列参数,我们知道串行队列传入的是NULL。根据代码可以发现,将dqa参数传入后,调用了_dispatch_queue_attr_to_info方法,生成了dispatch_queue_attr_info_t对象dqai

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);
    ...
}

我们可以发现dispatch_queue_attr_info_t是一个结构体位域的形式,里面包含了一些dispatch_qos_t等优先级的值。

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;

而查看_dispatch_queue_attr_to_info方法我们可以知道,当入参为NULL的时候,是直接返回一个空结构体的,也就是说当串行队列时,返回的是空值,所以代码后续对dqai的操作都是基于并发队列的,并通过此来进行判断取值

而并发队列,会根据传入的宏定义参数,通过位运算,来给dqai进行赋值,比较主要的有并发数(dqai_concurrent),优先级(dqai_qos)等

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;
    ....
    
    // 苹果的算法
	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;
    ...
}

dispatch_lane_t

我们虽然获取到了dispatch_queue_attr_info_t相关的值,但是它并不是最终返回的线程,只是我们用来获取一些配置的临时变量而已。通过看代码发现最终返回的是一个叫dqdispatch_lane_t对象,所以dispatch_lane_t应该是最终生成的队列,我们可以发现它是由_dispatch_object_alloc方法创建出来的。

但是_dispatch_object_alloc方法并没有开源,所以我们不得而知。不过_dispatch_object很像OC中的NSObject,是不是它也是一个抽象类似得存在呢。

dispatch_lane_t dq = _dispatch_object_alloc(vtable,
			sizeof(struct dispatch_lane_s));

dispatch_object_t(重点理解)

通过搜索dispatch_object,发现并找不到我们需要的多线程抽象类,不过我们发现,一般多线程的对象后面都有_t,所以我们找到了dispatch_object_t这个多线程的抽象类。

我们可以发现其是一个联合体,和我们isa的结果极其类似,里面包含了我们常用的很多信息。因为联合体互斥,这样做有利于内存的优化,和更好的实现多态。

  • dispatch_object_s结构体指针
  • dispatch_queue_s队列
  • dispatch_queue_attr_s参数值
  • dispatch_group_s
  • dispatch_semaphore_s信号量
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_queue_init

当我们创建出dispatch_lane_t对象dq后,紧接着就执行_dispatch_queue_init构造方法。

_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
			DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
			(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

这个方法的实现也比较简答,我们可以发现通过dispatch_queue_t dq = dqu._dq;取出队列后,对队列又进行了一系列的赋值,然后又返回了

_dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf,
		uint16_t width, uint64_t initial_state_bits)
{
	uint64_t dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(width);
	dispatch_queue_t dq = dqu._dq;

	dispatch_assert((initial_state_bits & ~(DISPATCH_QUEUE_ROLE_MASK |
			DISPATCH_QUEUE_INACTIVE)) == 0);

	if (initial_state_bits & DISPATCH_QUEUE_INACTIVE) {
		dq_state |= DISPATCH_QUEUE_INACTIVE + DISPATCH_QUEUE_NEEDS_ACTIVATION;
		dq->do_ref_cnt += 2; // rdar://8181908 see _dispatch_lane_resume
		if (dx_metatype(dq) == _DISPATCH_SOURCE_TYPE) {
			dq->do_ref_cnt++; // released when DSF_DELETED is set
		}
	}

	dq_state |= (initial_state_bits & DISPATCH_QUEUE_ROLE_MASK);
	dq->do_next = DISPATCH_OBJECT_LISTLESS;
	dqf |= DQF_WIDTH(width);
	os_atomic_store2o(dq, dq_atomic_flags, dqf, relaxed);
	dq->dq_state = dq_state;
	dq->dq_serialnum =
			os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
	return dqu;
}

dqai.dqai_concurrent ?DISPATCH_QUEUE_WIDTH_MAX : 1

我们发现在构造时有一个三目运算符,判断了dqai.dqai_concurrent,我们知道串行是没有dqai的,所以此时为1,就表示串行队列的并发数为1。

查看DISPATCH_QUEUE_WIDTH_MAX宏定义,我们发现为DISPATCH_QUEUE_WIDTH_FULL-2,即0xFFE,所以并发队列的最大并发数为0xFFE。至于-2则是因为-1是为了不饱和,在-1是因为DISPATCH_QUEUE_WIDTH_POOL为创建全局队列时候所使用的,避免相同

dq->do_targetq = tq;

执行完构造函数之后,接着又对dq进行了一些列赋值。但是如果每次创建线程,所有的属性都要重新赋值的话,是比较耗性能的,所以队列的创建是基于"模板"的,这个"模板"就是我们的do_targetq属性。

        ......
        // 标签
	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;

在方法中寻找,我们可以发现tq的创建是在根队列的基础上,获取到了优先级qosovercommit的赋值。

首先看qos的值,我们发现DISPATCH_QOS_UNSPECIFIED为0,且之前我们并没有赋值,所以一般情况下即执行DISPATCH_QOS_DEFAULT,为4,所以qos没指定的情况下为4。

接着看overcommit的值,根据上面dqai可以判断出,串行为1,并发为0

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

接着我们可以看_dispatch_get_root_queue函数,通过代码我们可以发现,执行的就是从根队列数组里通过下标来取出队列的逻辑,根据入参可以知道,下标为6或者7

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");
	}
	// 4-1= 3
	// 2*3+0/1 = 6/7
	return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

查看_dispatch_root_queues[]的定义可得如下代码。通过下标,我们可以得出如下结论。

  • 自定义串行队列target队列为com.apple.root.default-qos
  • 自定义并发队列target队列为com.apple.root.default-qos.overcommit
struct dispatch_queue_global_s _dispatch_root_queues[] = {
#define _DISPATCH_ROOT_QUEUE_IDX(n, flags) \
		((flags & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) ? \
		DISPATCH_ROOT_QUEUE_IDX_##n##_QOS_OVERCOMMIT : \
		DISPATCH_ROOT_QUEUE_IDX_##n##_QOS)
#define _DISPATCH_ROOT_QUEUE_ENTRY(n, flags, ...) \
	[_DISPATCH_ROOT_QUEUE_IDX(n, flags)] = { \
		DISPATCH_GLOBAL_OBJECT_HEADER(queue_global), \
		.dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE, \
		.do_ctxt = _dispatch_root_queue_ctxt(_DISPATCH_ROOT_QUEUE_IDX(n, flags)), \
		.dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL), \
		.dq_priority = flags | ((flags & DISPATCH_PRIORITY_FLAG_FALLBACK) ? \
				_dispatch_priority_make_fallback(DISPATCH_QOS_##n) : \
				_dispatch_priority_make(DISPATCH_QOS_##n, 0)), \
		__VA_ARGS__ \
	}
	_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,
	),
};

至此,我们自定义的一个串行或者并发队列就已经根据模板创建完成了

创建根队列

通过上面的分析,我们已经知道了自定义队列是根据根队列的模板来进行创建的。那么根队列又是何时创建的呢?

根据我们之前的分析iOS底层学习 - 从编译到启动的奇幻旅程(二),在dyld链接动态库时,会链接libdispatch库,运行libdispatcdispatch_queue_createh_init方法,找到了_dispatch_introspection_init方法,该方法就是创建根队列的主要方法。

我们可以看到该方法的主要实现就是一个for循环执行_dispatch_trace_queue_create方法,将上面_dispatch_root_queues[]数组中的队列进行一一创建。

并且通过_dispatch_trace_queue_create(&_dispatch_main_q);方法,创建了主队列,_dispatch_main_q即代表主队列。

void
_dispatch_introspection_init(void)
{
	_dispatch_introspection.debug_queue_inversions =
			_dispatch_getenv_bool("LIBDISPATCH_DEBUG_QUEUE_INVERSIONS", false);

	// Hack to determine queue TSD offset from start of pthread structure
	uintptr_t thread = _dispatch_thread_self();
	thread_identifier_info_data_t tiid;
	mach_msg_type_number_t cnt = THREAD_IDENTIFIER_INFO_COUNT;
	kern_return_t kr = thread_info(pthread_mach_thread_np((void*)thread),
			THREAD_IDENTIFIER_INFO, (thread_info_t)&tiid, &cnt);
	if (!dispatch_assume_zero(kr)) {
		_dispatch_introspection.thread_queue_offset =
				(void*)(uintptr_t)tiid.dispatch_qaddr - (void*)thread;
	}
	_dispatch_thread_key_create(&dispatch_introspection_key,
			_dispatch_introspection_thread_remove);
	_dispatch_introspection_thread_add(); // add main thread

	for (size_t i = 0; i < DISPATCH_ROOT_QUEUE_COUNT; i++) {
		_dispatch_trace_queue_create(&_dispatch_root_queues[i]);
	}
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
	_dispatch_trace_queue_create(_dispatch_mgr_q.do_targetq);
#endif
	_dispatch_trace_queue_create(&_dispatch_main_q);
	_dispatch_trace_queue_create(&_dispatch_mgr_q);
}

那么我们为什么能在任意地方通过dispatch_get_main_queue()取到主队列呢?

通过打印主队列,得到以下结果过,并可以得到其名称

<OS_dispatch_queue_main: com.apple.main-thread>

通过搜索名称,我们找到了主队列的定义。发现其为全局的静态变量,在里面有它的赋值

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,
};

那么我们的根队列创建后,并发数是多少呢,刚才我们知道了自定义队列的并发数为DISPATCH_QUEUE_WIDTH_MAX,即(DISPATCH_QUEUE_WIDTH_FULL - 2)。其中一个-1的预留就是为了给根队列,即#define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)。我们搜索可以发现在根队列的数组中,规定了此并发数

总结

  • 队列的创建分为串行和并行
  • 除了主队列,所有的队列的创建都是基于根队列
  • 串行队列并发数为1,自定义并发队列最大并发为0xFFE,根队列为0xFFF
  • 根队列的创建在dyld链接时,根据宏定义的数组创建
  • 主队列的创建在dyld链接时,是一个全局的静态串行队列变量,所以能够随时取用

参考

libdispatch源码