一口一口吃掉GCD (上)

从源码层面分析GCD的底层实现原理

Posted by Marco Liu on July 10, 2017

“Yeah I’m here’. ”

前言

在iOS的日常编码中,GCD很好地隔离了我们与线程(pthread)之间的直接交流,跟我们打交道的只有队列和Block,时间久了甚至感觉不到线程的存在,但是仍然阻止不了我对GCD底层原理了解的一腔热血。本文是对ios开发中GCD底层实现的总结,其实关于GCD底层的实现,网上相关文章多到数不过来,而且也非常不错,我也没有自信能比他们做的更好,因为毕竟每个人专研的东西不一样,在这里只作为个人的学习总结,同时也适用于用过GCD进行多线程编程的但未研究过源码的ios开发初学者。

正文

GCD版本选择

首先要做的就是选择要分析的GCD源码版本,因为越到后期的版本,工程越庞大,文件数激增,阅读起来很费神,这对了解底层机制十分不利,通过各个版本对比之下,最终选定了版本 libdispatch-84.5.5。这个版本虽然文件数量很少,但是五脏俱全,它仍然包含了所有的GCD的所有内容,所以我们可以先通过这个版本了解了原理后再去看最新版本,看苹果在GCD方面做了哪些优化。


知识储备

阅读 GCD 源码之前,需要了解一些相关知识,这样才能在读到源码时不至于一脸懵逼,进而影响理解。

1.DISPATCH_DECL

GCD 中对变量的定义大多遵循如下格式:

#define DISPATCH_DECL(name) typedef struct name##_s *name##_t

比如说非常常见的 DISPATCH_DECL(dispatch_queue);,它的展开形式是:

typedef struct dispatch_queue_s *dispatch_queue_t;

这行代码定义了一个 dispatch_queue_t 类型的指针,指向一个 dispatch_queue_s 类型的结构体。


2.原子操作

原子操作的原理就是给汇编指令加LOCK修饰。

#define dispatch_atomic_xchg(p, n)  __sync_lock_test_and_set((p), (n))

#define dispatch_atomic_cmpxchg(p, o, n)   __sync_bool_compare_and_swap((p), (o), (n))

#define dispatch_atomic_inc(p)              __sync_add_and_fetch((p), 1)

#define dispatch_atomic_dec(p)              __sync_sub_and_fetch((p), 1)

#define dispatch_atomic_add(p, v)       __sync_add_and_fetch((p), (v))

#define dispatch_atomic_sub(p, v)       __sync_sub_and_fetch((p), (v))

#define dispatch_atomic_or(p, v)        __sync_fetch_and_or((p), (v))

#define dispatch_atomic_and(p, v)       __sync_fetch_and_and((p), (v))
  • __sync_lock_test_and_set((p), (n))p设为value并返回p操作之前的值。

  • __sync_bool_compare_and_swap((p), (o), (n))这两个函数提供原子的比较和交换:如果p == o,就将n写入p(p代表地址,o代表oldValue,n代表newValue)

  • __sync_add_and_fetch((p), 1) 先自加1,再返回

  • __sync_sub_and_fetch((p), 1) 先自减1,再返回

  • __sync_add_and_fetch((p), (v)) 先自加v,再返回

  • __sync_sub_and_fetch((p), (v)) 先自减v,再返回

  • __sync_fetch_and_or((p), (v)) 先返回,再进行或运算

  • __sync_fetch_and_and((p), (v))先返回,再进行与运算


3.fastpath && slowpath

这是定义在 internal.h 中的两个宏:

#define fastpath(x) ((typeof(x))__builtin_expect((long)(x), ~0l))
#define slowpath(x) ((typeof(x))__builtin_expect((long)(x), 0l))

为了理解所谓的快路径和慢路径,我们需要先学习一点计算机基础知识。比如这段非常简单的代码:

if (x)
    return 1;
else
    return 39;

由于计算机并非一次只读取一条指令,而是读取多条指令,所以在读到 if 语句时也会把 return 1 读取进来。如果 x 为 0,那么会重新读取 return 39,重读指令相对来说比较耗时。

如过 x 有非常大的概率是 0,那么 return 1 这条指令每次不可避免的会被读取,并且实际上几乎没有机会执行, 造成了不必要的指令重读。当然,最简单的优化就是:

if (!x) 
    return 39;
else 
    return 1;

然而对程序员来说,每次都做这样的判断非常烧脑,而且容易出错。于是 GCC 提供了一个内置函数 __builtin_expect:

long __builtin_expect (long EXP, long C)

它的返回值就是整个函数的返回值,参数 C 代表预计的值,表示程序员知道 EXP 的值很可能就是 C。比如上文中的例子可以这样写:

if (__builtin_expect(x, 0)) 
    return 1;
else
    return 39;

虽然写法逻辑不变,但是编译器会把汇编代码优化成 if(!x) 的形式。

因此,在苹果定义的两个宏中,fastpath(x) 依然返回 x,只是告诉编译器 x 的值一般不为 0,从而编译器可以进行优化。同理,slowpath(x) 表示 x 的值很可能为 0,希望编译器进行优化。


4.dispatch_object_t

typedef union {
    struct dispatch_object_s *_do;          // dispatch_object_s结构体,这个是 GCD 的基类
    struct dispatch_continuation_s *_dc;    // 任务类型,通常 dispatch_async内的block最终都会封装成这个数据类型
    struct dispatch_queue_s *_dq;           // 任务队列,我们创建的对列都是这个类型的,不管是串行队列还是并发队列
    struct dispatch_queue_attr_s *_dqa;     // 任务队列的属性,任务队列的属性里面包含了任务队列里面的一些操作函数,可以表明这个任务队列是串行还是并发队列
    struct dispatch_group_s *_dg;           // GCD的group
    struct dispatch_source_s *_ds;          // GCD的sourece ,可以监测内核事件,文件读写事件和 socket 通信事件等
    struct dispatch_source_attr_s *_dsa;    // sourece的属性。
    struct dispatch_semaphore_s *_dsema;    // 信号量,如果了解过 pthread 都知道,信号量可以用来调度线程
} dispatch_object_t __attribute__((transparent_union));

可以看出,dispatch_object_t是一个联合体,所以当用dispatch_object_t 可以代表这个联合体内的所有数据类型。


5.dispatch_object_s

struct dispatch_object_s {
    DISPATCH_STRUCT_HEADER(dispatch_object_s, dispatch_object_vtable_s);
};

#define DISPATCH_STRUCT_HEADER(x, y)        \
    const struct y *do_vtable;      \   // 这个结构体内包含了这个 dispatch_object_s 的操作函数
    struct x *volatile do_next;     \   // 链表的 next
    unsigned int do_ref_cnt;        \   // 引用计数
    unsigned int do_xref_cnt;       \   // 外部引用计数
    unsigned int do_suspend_cnt;        \   // suspend计数,用作暂停标志,比如延时处理的任务,设置该引用计数之后;在任务到时后,计时器处理将会将该标志位修改,然后唤醒队列调度
    struct dispatch_queue_s *do_targetq;\   // 目标队列,就是当前这个struct x在哪个队列运行
    void *do_ctxt;                      \   // 上下文,我们要传递的参数
    void *do_finalizer

dispatch_object_s结构体是整个GCD的基类,地位十分重要,里面的一些成员已经在注释中做了基本的解释,随着剖析的逐步深入,我们会逐步了解这些成员存在的意义。

可以看出 do_vtable 的类型是dispatch_object_vtable_s:

struct dispatch_object_vtable_s {
    DISPATCH_VTABLE_HEADER(dispatch_object_s);
};

#define DISPATCH_VTABLE_HEADER(x)   \
    unsigned long const do_type;    \                           // 数据的具体类型
    const char *const do_kind; \                                // 数据的类型描述字符串
    size_t (*const do_debug)(struct x *, char *, size_t);   \   // 用来获取调试时需要的变量信息
    struct dispatch_queue_s *(*const do_invoke)(struct x *);\   // 唤醒队列的方法,全局队列和主队列此项为NULL
    bool (*const do_probe)(struct x *); \                       // 用于检测传入对象中的一些值是否满足条件
    void (*const do_dispose)(struct x *)                        // 销毁队列的方法,通常内部会调用 这个对象的 finalizer 函数

#define dx_type(x) (x)->do_vtable->do_type
#define dx_kind(x) (x)->do_vtable->do_kind
#define dx_debug(x, y, z) (x)->do_vtable->do_debug((x), (y), (z))
#define dx_dispose(x) (x)->do_vtable->do_dispose(x)
#define dx_invoke(x) (x)->do_vtable->do_invoke(x)
#define dx_probe(x) (x)->do_vtable->do_probe(x)

dispatch_queue

GCD的队列是GCD源码分析系列中的重点

队列其实就是一个用来提交 block 的对象,当 block 提交到队列中后,将按照 “先入先出(FIFO)” 的顺序进行处理。系统在 GCD 的底层会维护一个线程池,用来执行这些 block。对于队列来说,它只负责任务的调度,不负责任务的执行。Dispatch Queue通过结构体和链表,被实现为FIFO队列。

首先我们来分析一下dispatch_queue_s,它是一个结构体,定义如下:

struct dispatch_queue_s {
    DISPATCH_STRUCT_HEADER(dispatch_queue_s, dispatch_queue_vtable_s);
    DISPATCH_QUEUE_HEADER;
    char dq_label[DISPATCH_QUEUE_MIN_LABEL_SIZE];   // must be last
};

#define DISPATCH_STRUCT_HEADER(x, y)        \
    const struct y *do_vtable;      \   // 这个结构体内包含了这个 dispatch_object_s 的操作函数
    struct x *volatile do_next;     \   // 链表的 next
    unsigned int do_ref_cnt;        \   // 引用计数
    unsigned int do_xref_cnt;       \   // 外部引用计数
    unsigned int do_suspend_cnt;        \   // suspend计数,用作暂停标志,比如延时处理的任务,设置该引用计数之后;在任务到时后,计时器处理将会将该标志位修改,然后唤醒队列调度
    struct dispatch_queue_s *do_targetq;\   // 目标队列,就是当前这个struct x在哪个队列运行
    void *do_ctxt;                      \   // 上下文,我们要传递的参数
    void *do_finalizer

#define DISPATCH_QUEUE_HEADER \
    uint32_t dq_running; \
    uint32_t dq_width; \
    struct dispatch_object_s *dq_items_tail; \
    struct dispatch_object_s *volatile dq_items_head; \
    unsigned long dq_serialnum; \
    void *dq_finalizer_ctxt; \
    dispatch_queue_finalizer_function_t dq_finalizer_func

下面以 dispatch_queue_create 的源码为例:

dispatch_queue_create(const char *label, dispatch_queue_attr_t attr) {
    // 省略 label 相关的操作
    //dq_label代表队列的名字,且最长的长度不能超过DISPATCH_QUEUE_MIN_LABEL_SIZE = 64
	dispatch_queue_t dq;
	dq = _dispatch_alloc(DISPATCH_VTABLE(queue),
			sizeof(struct dispatch_queue_s) - DISPATCH_QUEUE_MIN_LABEL_SIZE -
			DISPATCH_QUEUE_CACHELINE_PAD + label_len + 1);
	_dispatch_queue_init(dq);
	if (fastpath(!attr)) {
		return dq;
	}
	if (fastpath(attr == DISPATCH_QUEUE_CONCURRENT)) {
		dq->dq_width = UINT32_MAX;
		dq->do_targetq = _dispatch_get_root_queue(0, false);
	} else {
		dispatch_debug_assert(!attr, "Invalid attribute");
	}
	return dq;
}

我们知道创建队列时, attr 属性有三个值可选,nilDISPATCH_QUEUE_SERIAL(实际上就是 nil) 或 DISPATCH_QUEUE_CONCURRENT。第一个 if 判断中,苹果认为串行队列,或者 NULL 参数更常见,因此 !attr 的值很有可能不为 0,这与上文的结论一致。

第二个判断中,参数几乎有只可能是 DISPATCH_QUEUE_CONCURRENT,因此 attr == DISPATCH_QUEUE_CONCURRENT 这个判断机会不会为 0,依然与 fastpath 的作用一致。

_dispatch_get_root_queue 会获取一个全局队列,它有两个参数,分别表示优先级和是否支持 overcommit。一共有四个优先级,LOWDEFAULTHIGHBACKGROUND,因此共有 8 个全局队列。带有 overcommit 的队列表示每当有任务提交时,系统都会新开一个线程处理,这样就不会造成某个线程过载(overcommit)。

这 8 个全局队列的序列号是 4-11,序列号为 1 的队列是主队列,2 是 manager 队列,用来管理 GCD 内部的任务(比如下文介绍的定时器),3 这个序列号暂时没有使用。队列 的 dq_width 被设置为 UINT32_MAX,表示这些队列不限制并发数。

作为对比,在 _dispatch_queue_init 中,并发数限制为 1,也就是串行队列的默认设置:

static inline void _dispatch_queue_init(dispatch_queue_t dq) {
	dq->do_next = DISPATCH_OBJECT_LISTLESS;
	dq->do_targetq = _dispatch_get_root_queue(0, true);
	dq->dq_running = 0;
	dq->dq_width = 1;
}

注意这行代码: dq->do_targetq = _dispatch_get_root_queue(0, true);,它涉及到 GCD 队列与 block 的一个重要模型,target_queue。向任何队列中提交的 block,都会被放到它的目标队列中执行,而普通串行队列的目标队列就是一个支持 overcommit 的全局队列,全局队列的底层则是一个线程池。