iOS底層學習 - 多線程之GCD隊列原理篇

通過前兩章的學習,咱們對多線程和GCD的使用已經有了瞭解,可是對於底層原理並不熟悉,咱們知道GCD使用主要有隊列和函數兩個參數,咱們就來一一探究,本章節咱們先來看一下GCD的隊列是如何建立的git

系列文章傳送門:github

iOS底層學習 - 多線程之基礎原理篇算法

iOS底層學習 - 多線程之GCD初探swift

簡介

咱們已經知道了,GCD中的隊列(FIFO)主要有如下四種:數組

咱們能夠經過dispatch_queue_create方法來生成一個dispatch_queue_t對象供GCD來使用,那麼在底層隊列是如何建立的呢?markdown

建立自定義隊列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);
}
複製代碼

因爲此方法比較長,且調用的相關方法比較多,咱們會截取一部分重點來說。app

dispatch_queue_attr_info_t

其中參數dispatch_queue_attr_t爲傳入的串行仍是並行隊列參數,咱們知道串行隊列傳入的是NULL。根據代碼能夠發現,將dqa參數傳入後,調用了_dispatch_queue_attr_to_info方法,生成了dispatch_queue_attr_info_t對象dqaiide

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源碼

相關文章
相關標籤/搜索