“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 属性有三个值可选,nil
、DISPATCH_QUEUE_SERIAL
(实际上就是 nil) 或 DISPATCH_QUEUE_CONCURRENT
。第一个 if 判断中,苹果认为串行队列,或者 NULL
参数更常见,因此 !attr
的值很有可能不为 0,这与上文的结论一致。
第二个判断中,参数几乎有只可能是 DISPATCH_QUEUE_CONCURRENT
,因此 attr == DISPATCH_QUEUE_CONCURRENT
这个判断机会不会为 0,依然与 fastpath
的作用一致。
_dispatch_get_root_queue
会获取一个全局队列,它有两个参数,分别表示优先级和是否支持 overcommit。一共有四个优先级,LOW
、DEFAULT
、HIGH
和 BACKGROUND
,因此共有 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 的全局队列,全局队列的底层则是一个线程池。