GCD 源码浅析

3,016 阅读20分钟

在iOS开发、面试时,是否对同步、异步、串行队列和并行队列的名词迷惑不解?当这些名词组合起来,同步串行队列,异步串行队列,同步并行队列以及异步并行队列,是否对这些情况在运行时的表现含糊不清,本文试着从源码的角度,来理清它们之间的差异,在开发面试中,更好的掌握gcd接口的用法。

1.数据结构

dispatch_queue_t

dispatch_queue_t是指向dispatch_queue_s的指针。通过代码对dispatch_queue_t的声明,在OC的语言环境中我们可以得到如下的展开。

DISPATCH_DECL(dispatch_queue);

// 最终展开如下
@protocol OS_dispatch_queue <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_queue> * __attribute__((objc_independent_class)) dispatch_queue_t

// OS_dispatch_object是由以下的宏定义
OS_OBJECT_DECL_CLASS(dispatch_object);
// 最终展开如下
@protocol OS_dispatch_object <NSObject>
@end
typedef NSObject<OS_dispatch_object> * __attribute__((objc_independent_class)) dispatch_object_t

这里OS_dispatch_queue的作用是什么呢?这只是OC中的一个虚拟类,我们可以创建一个Demo工程,创建一个队列,在Demo工程中通过断点查看创建队列所返回的dispatch_queue_t指针,可以看到对应的OC类是OS_dispatch_queue,并且没有任何的成员变量。我们在分配该类的时候,是通过calloc,按照dispatch_queue_s的结构分配的。所以我们可以对分配的对象按照dispatch_queue_s结构赋值,改变。也就是dispatch_queue_sOS_dispatch_queue是同一种对象的两种标识。有点类似toll-free bridge。当然和toll-free bridge的桥接类并不完全相同,两个类的方法和函数并不能互操作。这个类的isa指向的是dispatch_queue_vtable_s结构。描述这个队列进行一些操作需要调用的函数地址。

GCD中常见的结构和继承关系

queue_internal头文件中有注释说明了,下面两图分别是API使用者常使用的结构体和gcd源码内部使用的结构体的继承关系的图示。

dispatch-queue-create.png

dispatch-queue-create.png

从图1可以看到,我们常用的dispatch_queue_serial_tdispatch_queue_global_tdispatch_queue_concurrent_t都是继承于dispatch_queue_t,而主队列dispatch_queue_main_t是继承dispatch_queue_serial_t。因此我们熟知的主队列是串行队列这个结论从这里可以得到印证。

这里说明一下以_t_class_t_s结尾类型之间的关系,_t是一个指针类型,指向的结构对应_s的一种,_class_t是一个union类型,表示其指向的结构体可能是多个_s的其中一种,主要用于多种结构体之间的转换。

或许会有疑问C语言怎么来建立这种继承的关系呢?通过源码我们可以看到,GCD通过struct声明了这些结构,只是在声明各个结构体的时候通过一层层的宏定义,强制两个有继承关系的结构体声明一套相同的成员变量,这也增加了读代码的难度,获得一个结构的声明,就要颇费一番功夫。

还有一点就是如何在基类和子类之间转换呢?gcd中采用的是union这个结构,定义指针可能指向的类型,这里只是一个粗糙的面对对象的实现,在指针转换的时候还是需要注意的。这种结构体类型之间的转换是不安全的,也没有编译器帮助我们发现问题。

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;
 typedef struct dispatch_queue_s *dispatch_queue_t

这里声明了dispatch_object_t为一个联合体,成员是里面这些类型指针的一种。其实这也就和上面面对对象中的基类的概念相似,union中每个结构体指针都是其中一种"子类"的类型。 所以面对对象的编程和编程语言没有必然的关系,用C语言也可以写成优秀的面对对象的代码。 下面我们看下dispatch_queue_s的声明

struct dispatch_queue_s {
  struct dispatch_object_s _as_do[0];
  struct _os_object_s _as_os_obj[0];
  const struct dispatch_queue_vtable_s *do_vtable;
  int volatile do_ref_cnt;
   int volatile do_xref_cnt;
   struct dispatch_queue_s *volatile do_next;
   struct dispatch_queue_s *do_targetq;
   void *do_ctxt;
   void *do_finalizer;
   DISPATCH_UNION_LE(uint64_t volatile dq_state, dispatch_lock dq_state_lock, uint32_t dq_state_bits );
   void *__dq_opaque1;
   unsigned long dq_serialnum;
   const char *dq_label;
   DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags, const uint16_t dq_width, const uint16_t __dq_opaque2 );
   dispatch_priority_t dq_priority;
   union { 
	 struct dispatch_queue_specific_head_s *dq_specific_head;
   struct dispatch_source_refs_s *ds_refs;
   struct dispatch_timer_source_refs_s *ds_timer_refs;
   struct dispatch_mach_recv_refs_s *dm_recv_refs;
   };
   int volatile dq_sref_cnt;
  }

有几个成员变量这里做下说明:

  • _as_do_as_os_obj这种数组的用法,不符合C语言标准,是编译器扩展的用法,声明的变量不会分配空间,其作用是在不同的类型中间转换。
  • do_vtable这个是定义了如何操作这个队列的一些方法,比如怎么压入队列,怎么唤醒队列。主要用来解决队列的操作和队列结构之间的耦合,如果是OC类它的isa指向的也是vtable。
  • do_ref_cnt这个是记录引用计数的,和ARC的规则类似。
  • dq_label存放队列的名字。

2.串行队列和并行队列

我们最常用gcd的方法之一,就是创建一个队列,一般情况下使用如下代码

// 串行队列
dispatch_queue_t sq = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

// 并行队列
dispatch_queue_t cq = dispatch_queue_create("testcq", DISPATCH_QUEUE_CONCURRENT);

这里有两点需要关注:

其一,需要关注两个attr的类型。

第一个DISPATCH_QUEUE_SERIAL 是一个宏,展开为NULL,是默认属性。第二个即DISPATCH_QUEUE_CONCURRENT 展开为

DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t,_dispatch_queue_attr_concurrent)

这里插入说明一下dispatch_queue_attr_tdispatch_queue_attr_tdispatch_queue_attr_s的指针类型, DISPATCH_QUEUE_CONCURRENT对应的实例为_dispatch_queue_attr_concurrent,是一个全局初始化的变量。这个变量是放在了_dispatch_queue_attrs[]的数组中,所有的dispatch_queue_attr_t指针都会放置在这个数组中,每个指针在数组中的位置,会对应dispatch_queue_attr_info_t指向的结构体的信息,例如是不是并发,qos优先级等。同样一个dispatch_queue_attr_s实例的并发,优先级属性的不同,会对应在数组的不同位置。 以下是从dispatch_queue_attr_info_tdispatch_queue_attr_t的转换,可以看到,数组的index是通过dispatch_queue_attr_info_t的属性计算出来的,而从dispatch_queue_attr_tdispatch_queue_attr_info_t的转换就是这个计算过程的逆过程,这里不再赘述。

static dispatch_queue_attr_t
_dispatch_queue_attr_from_info(dispatch_queue_attr_info_t dqai)
{
	size_t idx = 0;

	idx *= DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;
	idx += dqai.dqai_overcommit;

	idx *= DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;
	idx += dqai.dqai_autorelease_frequency;

	idx *= DISPATCH_QUEUE_ATTR_QOS_COUNT;
	idx += dqai.dqai_qos;

	idx *= DISPATCH_QUEUE_ATTR_PRIO_COUNT;
	idx += (size_t)(-dqai.dqai_relpri);

	idx *= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;
	idx += !dqai.dqai_concurrent;

	idx *= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT;
	idx += dqai.dqai_inactive;

	return (dispatch_queue_attr_t)&_dispatch_queue_attrs[idx];
}

其二,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_queue_t,指向分配的队列结构。_dispatch_queue_create最终是通过_dispatch_lane_create_with_target函数来创建的。接下来我们看下_dispatch_lane_create_with_target的具体实现。

_dispatch_lane_create_with_target

_dispatch_lane_create_with_target(label, attr, DISPATCH_TARGET_QUEUE_DEFAULT, true)

首先入参分别为,创建队列的名称,attr是上面DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE_CONCURRENT指向的属性结构。DISPATCH_TARGET_QUEUE_DEFAULT宏展开即为NULL,最后一个参数legacytrue

static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
		dispatch_queue_t tq, bool legacy)
{
        //【1】
	dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
	qos = ...
	overcommit = ...

        //【2】
	if (!tq) {
		tq = _dispatch_get_root_queue(
				qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
				overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
		if (unlikely(!tq)) {
			DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
		}
	}
        //【3】
	legacy = ...
	const void *vtable = ...
	dqf = ...
	label = ...
        
        //【4】
	dispatch_lane_t dq = _dispatch_object_alloc(vtable, sizeof(struct dispatch_lane_s));
        //【5】
	_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
			DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
			(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
        //【6】
	dq->dq_label = label;
	dq->dq_priority = ...
        //【7】
	_dispatch_retain(tq);
	dq->do_targetq = tq;
        //【8】
	return dq._dq;
}

为了突出创建过程的主要步骤,截取的代码略去了一些参数的处理过程。下文通过指定代码行号分别来说明其大致的功能。

【1】根据dispatch_queue_attr_t,初始化qosovercommit

【2】设置tq变量。其中值得关注的是tq(目标队列,可以理解为我们创建的队列所依赖的全局队列)的获取,tq通过_dispatch_get_root_queue方法获得。通过_dispatch_get_root_queue的定义可以看到,gcd会创建一个全局的队列池,总共有16个,跳过了index为0的,1为main_q,2为mgr_q,3为mgr_root_q,4-15 为global queues,17为workloop_fallback_q。这里就是根据qosovercommit参数映射到线程池的index。从而获取这个队列。从这里也可以看出新创建的队列的目标队列都是全局的线程池。所以可以通过判断一个队列有没有do_targetq判断是不是系统的全局队列。

【3】根据条件,设置legacyvtabledqflabel

【4】_dispatch_object_alloc方法在内存堆中分配队列结构块,这里会指定队列的isa,指向对应的vtable,这里需要说明的是isado_vtable是同一个指针的两个名字,类似union结构。

【5】_dispatch_queue_init初始化结构中成员变量的值。

【6】设置队列的label和优先级。

【7】 把新创建的dq的目标队列设置为上面的tq。并且增加tq的引用计数。

【8】 最后返回队列指针,这就是整个创建队列的整个过程。

这里总结一下整个创建的流程,如下图所示。

未命名.001.png

创建队列就是在内存中分配队列的对象,并初始化成员变量,保存队列的性质。我们可以看到这里并没有和系统底层的线程建立关系,只有当我们通过dispatch_async或者dispatch_sync派发任务的时候,系统底层才会按需要创建线程执行任务。下文将探索异步派发和同步派发通过怎样的调用和底层的线程建立关系。

3.异步派发(dispatch_async)

dispatch_async(dispatch_queue_t dq, dispatch_block_t work)

异步派发的接口如上,其中有两个参数,dq就是添加任务的目标队列,work就是我们要执行的任务,是一个block类型。

这里dq就会有两种类型,就是前文所说的串行队列和并行队列。根据队列的不同,他们所执行的代码路径也是不同的。下面就是我们常常用来把一个任务提交到队列的代码片段。

// 串行队列的异步派发
dispatch_queue_t sq = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(sq, ^{
	NSLog(@"hello");
});

// 并行队列的异步派发
dispatch_queue_t cq = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(cq, ^{
	NSLog(@"hello");
});

下文先介绍两种队列派发的相同部分,然后再分别说明其中的不同之处。dispatch_async的具体实现如下。

dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
        //【9】
	dispatch_continuation_t dc = _dispatch_continuation_alloc();
	//【10】
        uintptr_t dc_flags = DC_FLAG_CONSUME;
	dispatch_qos_t qos;
        //【11】
	qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
        //【12】
	_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}

【9】_dispatch_continuation_alloc从名字可以看出是分配continuation结构的函数,具体实现中有thread关联的缓存机制。维护了一个供复用的continuation链表,可以避免频繁的分配内存,提升性能。

【10】dispatch_continuation_t这个是用来描述gcd任务,其中dc_flags描述了任务的类型,比如是barrierblockgroup等等。具体参见DC_FLAG_SYNC_WAITER及其他宏的定义。此处使用的DC_FLAG_CONSUME表示continuation资源在执行的时候已经释放了,这个标志一般在async里设置。

【11】_dispatch_continuation_init初始化continuation,这里我们看下具体实现

DISPATCH_ALWAYS_INLINE
static inline dispatch_qos_t
_dispatch_continuation_init_f(dispatch_continuation_t dc,
		dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
		dispatch_block_flags_t flags, uintptr_t dc_flags)
{
	pthread_priority_t pp = 0;
	dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
	dc->dc_func = f;
	dc->dc_ctxt = ctxt;
	pp = ...
	_dispatch_continuation_voucher_set(dc, flags);
	return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}

DISPATCH_ALWAYS_INLINE
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
		dispatch_queue_class_t dqu, dispatch_block_t work,
		dispatch_block_flags_t flags, uintptr_t dc_flags)
{
	void *ctxt = _dispatch_Block_copy(work);

	dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
	
	...

	dispatch_function_t func = _dispatch_Block_invoke(work);
	if (dc_flags & DC_FLAG_CONSUME) {
		func = _dispatch_call_block_and_release;
	}
	return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}

可以看到主要是设置continuation的flagsctxtfuncpriority的字段。其中我们要执行的任务block存放在了funcctxt字段。

【12】最后交给了_dispatch_continuation_async函数处理,下面是该函数的去除一些debug,性能检测代码的实现。

static inline void
_dispatch_continuation_async(dispatch_queue_class_t dqu,
		dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
	//... 一些调试,性能探针的处理
	return dx_push(dqu._dq, dc, qos);
}

dx_push是一个宏,这里最终展开为

dx_push(dqu._dq, dc, qos)
→ dx_vtable(dqu._dq)->dq_push(dqu._dq, dc, qos)
→ &(dqu._dq)->do_vtable->_os_obj_vtable->dq_push(dqu._dq, dc, qos)

这里对gcd中涉及的所有类型队列(串行队列,并行队列,管理队列等)进行了抽象,通过给不同队列赋值相应的dq_push(如何加入队列执行),dq_wakeup(怎么唤醒队列)等函数指针,从而操作某个队列时不必关心具体实现细节,达到调用和实现的分离解耦。和在面对对象编程中通过多态实现解耦的思想是相同的。

在创建队列的时候,_dispatch_queue_init会初始化对应的do_vtable。串行和并行队列的do_vtable指针指向的结构体如下。

DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_serial, lane,
	.do_type        = DISPATCH_QUEUE_SERIAL_TYPE,
	.do_dispose     = _dispatch_lane_dispose,
	.do_debug       = _dispatch_queue_debug,
	.do_invoke      = _dispatch_lane_invoke,

	.dq_activate    = _dispatch_lane_activate,
	.dq_wakeup      = _dispatch_lane_wakeup,
	.dq_push        = _dispatch_lane_push,
);

DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_concurrent, lane,
	.do_type        = DISPATCH_QUEUE_CONCURRENT_TYPE,
	.do_dispose     = _dispatch_lane_dispose,
	.do_debug       = _dispatch_queue_debug,
	.do_invoke      = _dispatch_lane_invoke,

	.dq_activate    = _dispatch_lane_activate,
	.dq_wakeup      = _dispatch_lane_wakeup,
	.dq_push        = _dispatch_lane_concurrent_push,
);

程序执行到此处,会根据传入disptach_async的队列,执行不同的代码。下文分别描述传入的为串行队列和并行队列时,disptach_async的执行过程。

在串行队列上异步派发

_dispatch_lane_push方法是把continuation插入队列的dq_items_taildq_items_head指向的双向链表中。最后_dispatch_lane_push会调用dq_wakeup的方法,唤醒队列。我们看到串行队列dq_wakeup指向_dispatch_lane_wakeup。这里简化的代码如下。

void
_dispatch_queue_wakeup(dispatch_queue_class_t dqu, dispatch_qos_t qos,
		dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target)
{
	dispatch_queue_t dq = dqu._dq;

	if (target) {
		uint64_t old_state, new_state, enqueue;
		enqueue = ...;
                qos = _dispatch_queue_wakeup_qos(dq, qos);
                old_state = ...;
                new_state = ...;

		if (likely((old_state ^ new_state) & enqueue)) {
			dispatch_queue_t tq = ...
                        
			return _dispatch_queue_push_queue(tq, dq, new_state);
		}
        }
}

通过检查state字段的状态,判断要不要把当前的队列添加到它的目标队列中。因为state可能会由多个线程访问,所以这里对字段的访问都要通过原子操作来进行,一般情况下最终_dispatch_queue_wakeup会走到_dispatch_queue_push_queue方法。

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_queue_push_queue(dispatch_queue_t tq, dispatch_queue_class_t dq,
		uint64_t dq_state)
{
#if DISPATCH_USE_KEVENT_WORKLOOP
	if (likely(_dq_state_is_base_wlh(dq_state))) {
		return _dispatch_event_loop_poke((dispatch_wlh_t)dq._dq, dq_state,
				DISPATCH_EVENT_LOOP_CONSUME_2);
	}
#endif // DISPATCH_USE_KEVENT_WORKLOOP
	return dx_push(tq, dq, _dq_state_max_qos(dq_state));
}

这里可以看到,对于队列的调度执行有两种方式,目前Apple平台大部分都使用的KEVENT_WORKLOOP这种方式。默认兜底的方式,是把当前创建的队列加入到系统的global队列中,形成一个数组的结构,gcd通过管理系统global队列进而管理新创建的队列任务。下文我们主要讨论KEVENT_WORKLOOP这种方式。

可以看到使用KEVENT_WORKLOOP方式,不需要目标队列这个参数。之后执行_dispatch_event_loop_poke,一如它的名字,像发扑克牌一样,系统通过一定的策略分发时间片给各自的队列,进而执行队列中的任务。这个方法里面做了兼容多种事件分发机制的操作,例如linux中使用的是pollselect系统调用实现的队列管理,还有其他一些分发的机制。而如今通用的KEVENT_WORKLOOP方式,最终会由_dispatch_kevent_workloop_poke来执行。

static void
_dispatch_kevent_workloop_poke(dispatch_wlh_t wlh, uint64_t dq_state,
		uint32_t flags)
{
	uint32_t kev_flags = KEVENT_FLAG_IMMEDIATE | KEVENT_FLAG_ERROR_EVENTS;
	dispatch_kevent_s ke;
	int action;
	action = _dispatch_event_loop_get_action_for_state(dq_state);

override:
        //【!】
	_dispatch_kq_fill_workloop_event(&ke, action, wlh, dq_state);
        
        //【!】
	if (_dispatch_kq_poll(wlh, &ke, 1, &ke, 1, NULL, NULL, kev_flags)) {
		// ... error handler
	}

	if (!(flags & DISPATCH_EVENT_LOOP_OVERRIDE)) {
		// Consume the reference that kept the workloop valid
		// for the duration of the syscall.
		return _dispatch_release_tailcall((dispatch_queue_t)wlh);
	}
	if (flags & DISPATCH_EVENT_LOOP_CONSUME_2) {
		return _dispatch_release_2_tailcall((dispatch_queue_t)wlh);
	}
}

这个方法中有两个重要的方法。_dispatch_kq_fill_workloop_event(&ke, action, wlh, dq_state)_dispatch_kq_poll(wlh, &ke, 1, &ke, 1, NULL, NULL, kev_flags)

其中_dispatch_kq_fill_workloop_event是根据队列的属性分配一个dispatch_kevent_s的结构,包装了请求内核的操作,其中NOTE_WL_THREAD_REQUEST表明想要请求内核分配一个线程。

_dispatch_kq_poll的实现如下,这里去掉了一些异常处理和无效分支的代码。

DISPATCH_NOINLINE
static int
_dispatch_kq_poll(dispatch_wlh_t wlh, dispatch_kevent_t ke, int n,
		dispatch_kevent_t ke_out, int n_out, void *buf, size_t *avail,
		uint32_t flags)
{
	bool kq_initialized = false;
	int r = 0;
        //【12】
	dispatch_once_f(&_dispatch_kq_poll_pred, &kq_initialized, _dispatch_kq_init);
  
retry:
	{
		flags |= KEVENT_FLAG_WORKLOOP;
		if (!(flags & KEVENT_FLAG_ERROR_EVENTS)) {
			flags |= KEVENT_FLAG_DYNAMIC_KQ_MUST_EXIST;
		}
		r = kevent_id((uintptr_t)wlh, ke, n, ke_out, n_out, buf, avail, flags);
#endif // DISPATCH_USE_KEVENT_WORKLOOP
	}
	if (unlikely(r == -1)) {
		//error handler
	}
	return r;
}

这里通过上个函数创建的dispatch_kevent_s结构,发起系统调用kevent_id。等待请求事件调度。这里kqueuekevent系统调用主要任务是请求系统分配线程,并且当请求完成的时候,通知到用户态。kqueue是一种可扩展的事件通知接口,类似linux中的epoll

【12】这里会调用一次_dispatch_kq_init方法,该方法处理一些初始化kqueuerootqueuemanagerqueue相关操作。可以通过代码看到该方法会调用到_dispatch_root_queues_init_once方法。 而这个方法又调用了_pthread_workqueue_init_with_kevent_pthread_workqueue_init_with_kevent方法是在pthread库中实现的。在这个库里会处理和内核的交互,最终会保证我们请求的kqueuekevent系统调用完成时候,会回调到_dispatch_workloop_worker_thread这个方法入口,具体这个过程中间的实现,这里不再详述,有兴趣的可看下pthread和内核的源码。

_dispatch_workloop_worker_thread将通过队列的do_vtable调用队列的invoke函数,串行队列就是_dispatch_lane_invoke函数,最终从队列的任务链表中取出要执行的任务,并处理任务执行完毕队列的状态。调用堆栈如下。

image.png

_dispatch_lane_invoke中会调用_dispatch_lane_serial_drain方法,其主要功能就是,将串行队列中加入的任务根据先进先出的顺序从链表中遍历执行。

在并行队列上异步派发

下面是_dispatch_lane_concurrent_push的实现。并行队列最终调用了_dispatch_continuation_redirect_push方法添加任务.

void
_dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
		dispatch_qos_t qos)
{
	// <rdar://problem/24738102&24743140> reserving non barrier width
	// doesn't fail if only the ENQUEUED bit is set (unlike its barrier
	// width equivalent), so we have to check that this thread hasn't
	// enqueued anything ahead of this call or we can break ordering
	if (dq->dq_items_tail == NULL &&
			!_dispatch_object_is_waiter(dou) &&
			!_dispatch_object_is_barrier(dou) &&
			_dispatch_queue_try_acquire_async(dq)) {
		return _dispatch_continuation_redirect_push(dq, dou, qos);
	}

	_dispatch_lane_push(dq, dou, qos);
}

_dispatch_continuation_redirect_push实现如下。

static void
_dispatch_continuation_redirect_push(dispatch_lane_t dl,
		dispatch_object_t dou, dispatch_qos_t qos)
{
        dou._dc = ...
	dispatch_queue_t dq = dl->do_targetq;
	if (!qos) qos = _dispatch_priority_qos(dq->dq_priority);
	dx_push(dq, dou, qos);
}

这里重新包装了continuation,替换了continuationinvoke指向的函数实现,并且把重新包装好的continuation加入到系统的全局队列中,加入全局队列调用,所以并行队列的任务链表一直是空的。

_dispatch_root_queue_push方法,

DISPATCH_NOINLINE
void
_dispatch_root_queue_push(dispatch_queue_global_t rq, dispatch_object_t dou,
		dispatch_qos_t qos)
{
#if HAVE_PTHREAD_WORKQUEUE_QOS
	if (_dispatch_root_queue_push_needs_override(rq, qos)) {
		return _dispatch_root_queue_push_override(rq, dou, qos);
	}
#else
	(void)qos;
#endif
	_dispatch_root_queue_push_inline(rq, dou, dou, 1);
}

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_root_queue_push_inline(dispatch_queue_global_t dq,
		dispatch_object_t _head, dispatch_object_t _tail, int n)
{
	struct dispatch_object_s *hd = _head._do, *tl = _tail._do;
	if (unlikely(os_mpsc_push_list(os_mpsc(dq, dq_items), hd, tl, do_next))) {
		return _dispatch_root_queue_poke(dq, n, 0);
	}
}

该方法把重新包装的continuation加入到root队列中,然后调用_dispatch_root_queue_poke,启动根队列的任务调度。之后会调用_dispatch_root_queue_poke_slow方法,对并行队列任务进行调度处理。

DISPATCH_NOINLINE
static void
_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)
{
	int remaining = n;
	int r = ENOSYS;

	_dispatch_root_queues_init();

#if !DISPATCH_USE_INTERNAL_WORKQUEUE
#if DISPATCH_USE_PTHREAD_ROOT_QUEUES
	if (dx_type(dq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE)
#endif
	{
		r = _pthread_workqueue_addthreads(remaining,
				_dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
		(void)dispatch_assume_zero(r);
		return;
	}
#endif // !DISPATCH_USE_INTERNAL_WORKQUEUE
}

这个函数会调用_pthread_workqueue_addthreads(这个方法在pthread库中)。_pthread_workqueue_addthreads这个方法会通过系统调用向workqueue请求分配线程,系统调用完成时,会回调root_queue_init初始化的时候,注册的_dispatch_worker_thread2方法,堆栈如下图。

image (1).png

然后_dispatch_root_queue_drain把前面加入的continuation取出并执行,最终调用到我们传入的block中的代码。

小结

下图总结了以上异步派发针对两种队列执行过程。

dispatch-queue-create.png

由上可知,异步派发的实现涉及了pthread库和内核提供的系统调用,如下图。

未命名.001.png

以上可以看到,串行和并行队列都会发起系统调用,请求线程。

对于串行队列是通过kqueue系统调用发起的,而并行队列只是通过添加线程。造成这个差异的原因是因为,如果我们向一个串行队列加入多个任务,这个任务是需要在一个线程中执行的,这样才能保证同一时间只能有一个任务在执行,而发起kevent_id系统调用的时候有会传入一个dispatch_kevent_s结构体来标识队列对应的线程,每次添加任务,可以保证加入的是一个线程中。

而对于并行队列,因为同一时间可以执行多个队列中的任务,所以添加任务的时候直接可以添加一个新线程,当然不可能每添加一个任务就开辟一个新线程,当分配的线程执行完一个并行队列任务之后,将可以用来执行之后新加入的任务,类似一个线程池的概念,在代码里我们可以看到没有针对并行队列的并发量做限制,所以这也是为什么我们在使用并发队列的时候,没有直接指定并发数量的API。

4.同步派发(dispatch_sync)

接下来看下同步派发的实现,同样派发的队列可以是串行,也可以是并行的。

dispatch_queue_t sq = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(sq, ^{
	NSLog(@"hello");
});

dispatch_queue_t cq = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(cq, ^{
	NSLog(@"hello");
});

dispatch_sync的实现如下。

DISPATCH_NOINLINE
void
dispatch_sync(dispatch_queue_t dq, dispatch_block_t work)
{
	uintptr_t dc_flags = DC_FLAG_BLOCK;
	if (unlikely(_dispatch_block_has_private_data(work))) {
		return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
	}
	_dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}

DISPATCH_NOINLINE
static void
_dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func,
		uintptr_t dc_flags)
{
	_dispatch_sync_f_inline(dq, ctxt, func, dc_flags);
}

其中_dispatch_sync_f_inline的实现如下

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
		dispatch_function_t func, uintptr_t dc_flags)
{
	if (likely(dq->dq_width == 1)) {
		return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
	}

	if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
		DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
	}

	dispatch_lane_t dl = upcast(dq)._dl;
	// Global concurrent queues and queues bound to non-dispatch threads
	// always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
	if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
		return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags);
	}

	if (unlikely(dq->do_targetq->do_targetq)) {
		return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
	}
	_dispatch_introspection_sync_begin(dl);
	_dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
			_dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
}

从上面串行队列创建过程中,_dispatch_queue_init的调用,可以看到dq->dq_width == 1。所以对串行队列执行同步操作,最终是调用_dispatch_barrier_sync_f的方法。 从这里可以得出,如果我们加入的是并行队列,会直接调用_dispatch_sync_invoke_and_complete函数处理。下面分成在串行队列和并行队列两部分来说明。

在串行队列上同步派发

在串行队列上接下来会调用

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_barrier_sync_f_inline(dispatch_queue_t dq, void *ctxt,
		dispatch_function_t func, uintptr_t dc_flags)
{
	dispatch_tid tid = _dispatch_tid_self();

	dispatch_lane_t dl = upcast(dq)._dl;
        
	if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))) {
		return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
				DC_FLAG_BARRIER | dc_flags);
	}

	if (unlikely(dl->do_targetq->do_targetq)) {
		return _dispatch_sync_recurse(dl, ctxt, func,
				DC_FLAG_BARRIER | dc_flags);
	}
	_dispatch_lane_barrier_sync_invoke_and_complete(dl, ctxt, func
			DISPATCH_TRACE_ARG(_dispatch_trace_item_sync_push_pop(
					dq, ctxt, func, dc_flags | DC_FLAG_BARRIER)));
}

队列没有多重依赖,所以不需要递归的调用。_dispatch_introspection_sync_begin只是帮助调试,性能统计的函数,这里不用特别关注。顺便提及,我们知道如果我们在一个串行队列的任务重再次提交同一个串行队列一个同步任务,会造成死锁,这里第二次调用dispatch_sync执行到这里会获取不到barrier_sync,走到_dispatch_sync_f_slow。其中的__DISPATCH_WAIT_FOR_QUEUE__函数会检查到要执行任务的队列和被锁的队列是同一个队列,然后触发crash。 回到正题,之后会走到_dispatch_lane_barrier_sync_invoke_and_complete

DISPATCH_NOINLINE
static void
_dispatch_lane_barrier_sync_invoke_and_complete(dispatch_lane_t dq,
		void *ctxt, dispatch_function_t func DISPATCH_TRACE_ARG(void *dc))
{
	_dispatch_sync_function_invoke_inline(dq, ctxt, func);
	_dispatch_trace_item_complete(dc);
	if (unlikely(dq->dq_items_tail || dq->dq_width > 1)) {
		return _dispatch_lane_barrier_complete(dq, 0, 0);
	}

	const uint64_t fail_unlock_mask = ...;
	uint64_t old_state, new_state;

	// similar to _dispatch_queue_drain_try_unlock
	os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, release, {
		new_state  = old_state - DISPATCH_QUEUE_SERIAL_DRAIN_OWNED;
		new_state &= ~DISPATCH_QUEUE_DRAIN_UNLOCK_MASK;
		new_state &= ~DISPATCH_QUEUE_MAX_QOS_MASK;
		if (unlikely(old_state & fail_unlock_mask)) {
			os_atomic_rmw_loop_give_up({
				return _dispatch_lane_barrier_complete(dq, 0, 0);
			});
		}
	});
	if (_dq_state_is_base_wlh(old_state)) {
		_dispatch_event_loop_assert_not_owned((dispatch_wlh_t)dq);
	}
}

_dispatch_sync_function_invoke_inline这个方法会保存当前线程的信息,然后在当前线程执行提交的block任务,恢复线程信息。最后处理任务执行完成之后的清理工作。

在并行队列上同步派发

有前文可知,在_dispatch_sync_f_inline函数指,串行队列会执行到_dispatch_sync_invoke_and_complete

DISPATCH_NOINLINE
static void
_dispatch_sync_invoke_and_complete(dispatch_lane_t dq, void *ctxt,
		dispatch_function_t func DISPATCH_TRACE_ARG(void *dc))
{
	_dispatch_sync_function_invoke_inline(dq, ctxt, func);
	_dispatch_trace_item_complete(dc);
	_dispatch_lane_non_barrier_complete(dq, 0);
}

_dispatch_sync_function_invoke_inline就是在当前线程,首先保存线程的上下文,然后执行提交的任务,回复之前执行的上下文。 _dispatch_lane_non_barrier_complete处理执行完任务的清理工作,是否释放队列等等的操作。

小结

dispatch-queue-create.png

以上可以看出同步派发相比异步派发来说,其实现简单了许多。同步派发就是在当前的线程直接执行提交的任务。对于串行,因为不能同时执行多个,需要判断当前队列是不是有正在执行的任务,所以处理起来稍复杂些。而并行队列,不用关心当前是不是有任务正在执行,则直接在当前线程执行即可。当然任务执行完毕,都需要设置处理队列的状态。

5.总结

本文简单介绍了串行和并行队列创建的实现,以及提交同步和异步任务的执行过程。其中的实现细节十分丰富,这里没能做到面面俱到,并且很多细节我也还需要探究。整套gcd的实现不仅仅是libdispatch中的代码,它还紧密的依赖pthread和xnu内核的实现。

回到开始提出的问题。并行和串行,同步和异步是两组独立的概念,并行和串行是描述我们在同一个队列中提交多个任务,任务可以的执行方式。并行就是提交的任务可以在多个线程上同时执行,串行就是队列中的任务只能在特定的线程上一个一个的执行。

同步和异步描述的是当前的执行任务和添加到队列中的任务之间的关系。同步派发就是当前的任务暂停,直到添加到队列中的任务执行完成,当前的任务才继续执行。通过dispatch_sync的实现也可以看出,我们提交的任务一般情况下就是在当前的线程执行,然后返回到调用处,继续执行之后的代码。相对应的异步派发,就是只是把任务提交到队列中,然后继续执行当前的任务,至于什么时候执行提交到队列中的任务,由底层的线程调度来控制。并且一般情况下不会在当前线程中执行队列的任务(如果在当前的串行队列中执行,可能会导致死锁)。通过其实现我们可以看到,串行队列上的异步派发,在执行完kevent_id系统调用之后,就会返回到dispatch_async调用处,继续执行之后的代码了,而提交的任务,在一个新的线程中执行,何时执行队列中的任务,取决于kqueue在内核中的实现。

hi, 我是快手电商的长天

快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~

热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~

内部推荐请发简历至 >>>我们的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名哦~ 😘