iOS开发:多线程之GCD基础知识

iOS

写在前面

llibdispatch是苹果开发的一套C接口的库(苹果官方称之为GCD,Grand Central Dispatch),多线程技术的封装是其核心之一。GCD内部管理线程池,对上层以队列的形式提供操作接口,任务则以block的形式提交到队列,最终由GCD决策如何将任务派发到线程执行。

之前有翻译过raywenderlich中关于GCD的两篇文章,里面比较清楚的讲解了串行、并发的概念以及基本的API说明。今天来重新整理一遍libdispatch相关内容,其缘由是前不久项目中发现对队列与线程的一些误解。记录与分享之。

背景知识

并行与并发

parallelism与concurrency的区别,这个在raywenderlich也有说明。简而言之,并行(parallelism)在计算机领域则指的是机器指令的同时执行,也就是多个处理器执行;并发(concurrency)指的是逻辑层面的同时进行,其对立面是sequentially、任务间有先后顺序的处理方式。不太恰当的举个例子,项目开发上我们不同团队之间会是并行开发,比如后端、前端、美术团队是同时进行;而至于你,则一边听着音乐、一边码着代码。前者强调的是物理线路的分离,后者强调的是逻辑依赖的分离。并行的必要条件是并发,但并发不是并行的充分条件。放到所举栗子中,项目中不同团队的并行开发要求不同团队中的任务是没有依赖的,而就算任务不存在依赖,如果没有这样的团队划分(比如人员紧缺、职责混乱)也无法达成并行开发。

同步与异步

synchronous与asynchronous,同步/异步在计算机领域多线程的语境下,通常用来用来描述两个(及以上)操作之间的协作关系。程序执行中的状态通常称之为上下文(Context),切换进程、线程以及函数调用都需要切换上下文。我们知道,每个线程是一个无分叉的CPU命令执行序列。我们发起一个过程调用时,函数调用栈会将上一个函数的上下文信息(栈幁,活动记录,可参考wiki)push到栈中,直到函数处理结束后栈幁pop出,指令执行指针(如PC或IP)才会被重置到发起调用前代码的下一条指令。同步调用或异步调用(回调),描述的是消息的通知模式,也就是被调用的函数过程的执行结果,是通过返回值还是通过回调函数/闭包的形式来通知调用者,前者是同步调用后者是异步调用。这是两个函数过程的不同的协作模式。所以,像“异步函数”这种是很不恰当而奇怪的称呼。

所以所谓“同步队列”、“异步队列”这些是误用。

多线程方案

按抽象层次从低到高列举简单介绍如下:

  1. pthread 符合POSIX标准的libc(或glibc)中的线程库实现
  2. NSThread 苹果的基于pthread实现的OC接口的线程管理类
  3. GCD 苹果实现的C接口的任务/队列管理,线程池管理
  4. NSOperation 基于GCD的OC接口的任务管理,添加了任务依赖等特性

目前最常用莫过于GCD与NSOperation了。

GCD基础知识

串行、并发队列

GCD中所有队列都是FIFO的方式对任务的出列进行管理,可以分为两大类:串行队列、并发队列。

其中串行执行队列,必须等每个出列的任务执行完后才将下一个任务出列;而并发执行队列则是当前任务出列后,无需等待执行完毕,在并发数允许的情况下,可以将下一个任务出列。

需要先提一下,GCD中的任务dispatch通常会有两组接口,一组是dispatch[_xxx]_sync(queue, task),一组是dispatch[_xxx]_async(queue, task),其功能都是将任务task提交到队列queue中执行。这里的syncasync的概念可能与"过程阻塞调用"更接近。前者是派发同步调用的任务,后者派发异步调用的任务。dispatch[_xxx]_sync(queue, task)的函数调用,会想办法实现,在task执行完之后才继续执行调用前的下一条指令;而dispatch[_xxx]_async(queue, task)则仅是将task添加到queue中,然后该函数执行完毕继续下一条指令,至于task`什么时候执行它不关心。当并发数为1时,并发队列等同于串行队列。

GCD的队列细分可按下表所示划分为4种(见下表示)。主线程队列是个特殊的串行队列,可通过dispatch_get_main_queue()来取得。对于并发队列,提供提供了几个不同优先级的全局的并发队列。用户可自定义队列,接口为dispatch_create_queue(label, attributes)

队列 类型 创建/获取 备注
主队列 串行 dispatch_get_main_queue()
全局并发队列 并发 dispatch_get_global_queue(priority, flag)
自定义串行队列 串行 dispatch_create_queue(“com.queue.name”, NULL) 需锚定
自定义并发队列 并发 dispatch_create_queue(“com.queue.name”, DISPATCH_QUEUE_CONCURRENT) 需锚定

示例:

1
2
3
4
// 创建串行队列
dispatch_create_queue("com.queue.serial", NULL)
// 创建并发队列
dispatch_create_queue("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT)
线程池

GCD对上层提供的概念是队列的概念,并自身维护了线程池,libdispatch会按需将队列中的任务派发到具体的线程中执行。

但队列与线程并非一一对应的关系。这是个很容易被误解的地方。(当然我这次也踩了这个坑)

提交到同一个队列的任务,并不一定在同一个线程中执行。即使是串行队列。

举个实际的例子:

为了解决数据访问的不一致问题(实际是Realm数据库读写),我们创建了一个串行队列syncqueue,会有dispatch_sync()以及dispatch_async()两种调用。我们的预期是,这不是串行队列吗?上面两种调用,是会在同一个线程中执行的。你觉得呢?syncasync的调用并不能保证是同一个线程中的操作,而Realm本身为速度而生、是一种mvcc结构,是允许多线程的同时读写,它的数据一致性是通过数据更新来实现的。结果可想而知,调用了delete异步写入之后,并不能反映到随后的同步query结果中。

事实上,是我们混淆了队列与线程的概念、关系。在上面例子中,syncasync的操作依然是遵循队列FIFO串行出列的特性的,但sync操作往往被dispatch到调用线程中执行,而async操作往往被dispatch到一个新的线程中执行。(可通过下面的测试进行验证)

所以务必记住线程池的概念,区分队列与线程。

主线程队列

主线程队列有什么特殊?简要的说,这是个跟应用主线程关联的队列。

首先,只有主线程队列结合到主runloop中去了,主线程队列提交的任务一定在主线程执行。因为是串行队列,所有任务按提交的时间先后执行,在每次runloop循环中检查是否有任务并执行。非线程安全的UI操作,默认都需要在主线程(队列)中执行。

再者,在主队列异步提交的任务,也只会在主线程执行,而不会开辟新的线程执行。这是个容易被忽略的常识。因为对于异步提交到非主线程队列的任务,通常会被提交到另一个具体线程(与当前线程不同)执行。

目标队列

主线程队列任务会在主线程中执行,全局并发队列中任务会被派发到具体线程中执行。但自定义队列中的任务不会直接被提交到具体线程中,而是需要锚定以上两种队列,称之为目标队列。自定义队列中的任务会被提交到目标队列中。

可使用dispatch_set_target_queue(object, targetqueue)来指定目标队列。

一些坑点
  1. dispatch[_xxx]_sync的不当使用导致死锁

    在串行队列中,派发同步调用的任务,必定导致死锁。例如在主线程中(或dispatch_async()到mainQueue的任务block中),调用dispatch[_xxx]_sync必定死锁;在其他串行队列同样如此(但并发队列不会,虽然官方不建议)。

  2. dispatch_after(delta, queue, task),在指定时间追加任务到队列中。

测试

以下是一些基本测试,通过这些测试代码,你可能更好的体会串行/并发、同步/异步的概念。以下代码均在主线程调起。其中,syncq、syncq2为2个新建的串行执行队列,asyncq为新建的并发执行队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);
// 全部输出为:[Thread][0]: <NSThread: 0x60000007b440>{number = 1, name = main},后文简称main
dispatch_sync(syncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_sync(asyncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_async(syncq, ^{
// 以下输出全为:[Thread][1]: <NSThread: 0x6000002653c0>{number = 3, name = (null)}
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_sync(asyncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);
// [Thread][0]: <NSThread: 0x60000007b440>{number = 1, name = main}
dispatch_sync(syncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_sync(dispatch_get_main_queue(), ^{// 死锁
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_sync(syncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_async(asyncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});

输出为:
[Thread][0]: <NSThread: 0x6000000701c0>{number = 1, name = main}
[Thread][1]: <NSThread: 0x6000000701c0>{number = 1, name = main}
[Thread][4]: <NSThread: 0x6000000701c0>{number = 1, name = main}
[Thread][2]: <NSThread: 0x60000007d040>{number = 3, name = (null)}
[Thread][3]: <NSThread: 0x6000000701c0>{number = 1, name = main}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_sync(syncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_async(asyncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(asyncq, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});

输出:
[Thread][0]: <NSThread: 0x60800007ea80>{number = 1, name = main}
[Thread][1]: <NSThread: 0x60800007ea80>{number = 1, name = main}
[Thread][4]: <NSThread: 0x60800007ea80>{number = 1, name = main}
[Thread][2]: <NSThread: 0x60000026c840>{number = 3, name = (null)}
[Thread][3]: <NSThread: 0x60000026c840>{number = 3, name = (null)}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_sync(asyncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_async(syncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq, ^{// 死锁
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});

输出:
[Thread][0]: <NSThread: 0x600000077980>{number = 1, name = main}
[Thread][1]: <NSThread: 0x600000077980>{number = 1, name = main}
[Thread][4]: <NSThread: 0x600000077980>{number = 1, name = main}
[Thread][2]: <NSThread: 0x608000262d80>{number = 3, name = (null)}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_sync(asyncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_async(syncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(syncq, ^{//syncq等待?
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});

输出:
[Thread][0]: <NSThread: 0x60800007b740>{number = 1, name = main}
[Thread][1]: <NSThread: 0x60800007b740>{number = 1, name = main}
[Thread][2]: <NSThread: 0x600000266700>{number = 3, name = (null)}
[Thread][3]: <NSThread: 0x600000266700>{number = 3, name = (null)}
[Thread][4]: <NSThread: 0x60800007b740>{number = 1, name = main}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSLog(@"[Thread][0]: %@",[NSThread currentThread]);//main
dispatch_sync(asyncq, ^{
NSLog(@"[Thread][1]: %@",[NSThread currentThread]);

dispatch_async(syncq, ^{
NSLog(@"[Thread][2]: %@",[NSThread currentThread]);

dispatch_sync(syncq2, ^{
NSLog(@"[Thread][3]: %@",[NSThread currentThread]);
});
});

dispatch_sync(asyncq, ^{
NSLog(@"[Thread][4]: %@",[NSThread currentThread]);
});
});

输出:
[Thread][0]: <NSThread: 0x600000067e80>{number = 1, name = main}
[Thread][1]: <NSThread: 0x600000067e80>{number = 1, name = main}
[Thread][4]: <NSThread: 0x600000067e80>{number = 1, name = main}
[Thread][2]: <NSThread: 0x600000073580>{number = 3, name = (null)}
[Thread][3]: <NSThread: 0x600000073580>{number = 3, name = (null)}

本文主要介绍GCD的一些基本背景知识以及基础知识,更详细的介绍以及拓展知识,将在后续系列中介绍。

本文完。

Author: Jason

Permalink: http://blog.knpc21.com/ios/mutilthread-gcd-basic/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments