[toc]

引子

本篇内容大多来自网络上的一些文章,希望做一个 📝 和参考,如有出错的地方希望指正。

起因是公司内部的同学在开发中遇到了 FMDB 的一个 Crash,查看源码:

 1- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {
 2#ifndef NDEBUG
 3    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
 4    // ...
 5#endif
 6    dispatch_sync(_queue, ^() {
 7        FMDatabase *db = [self database];        
 8        block(db);
 9	    // ...
10    });
11}

发现 FMDB 内部居然是使用了 dispatch_sync,我们从小就被教育要少用 sync 而这里居然明目张胆的使用了。。。

看来一定是我哪里有所误解。

GCD 基础

GCD 属于苹果核心系统,它封装了多线程操作并在其之上增加了任务容器 — Dispatch queues。可以说,Apple 希望淡化线程概念,强化易理解的任务队列。

并行和并发

在多线程模型的讨论中,搞明白 并行和并发 这两个概念十分重要,关于它们的解释也是各有不同,这里参考 StackOverflow 上的回答

Concurrency

Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn’t necessarily mean they’ll ever both be running at the same instant. For example, multitasking on a single-core machine.

01-Concurrency

Parallelism

Parallelism is when tasks literally run at the same time, e.g., on a multicore processor.

02-Parallelism

借用 内核恐慌 中 Rio 的描述:

并发和并行是一种计算模型,使得计算机能够在同一时间处理多个任务;

并发表示逻辑概念上的同时,并行表示物理概念上的同时。

简单来说,若说两个任务 A 和 B 并发执行,则表示任务 A 和任务 B 在同一时间段里被执行(更多的可能是二者交替执行);

若说任务 A 和 B 并行执行,则表示任务 A 和任务 B 在同时被执行(这要求计算机有多个运算器)。

一句话总结:并行要求并发,但并发并不能保证并行。

关于并发和并行,Grand Central Dispatch In-Depth: Part 1/2 中有更详细生动的图文解释。

Dispatch Queues

A dispatch queue is an object-like structure that manages the tasks you submit to it. All dispatch queues are first-in, first-out data structures.

关于什么是 dispatch queue 苹果文档 已经给出了明确的定义,它是以 FIFO 的顺序来调度任务的一种数据结构。

GCD 提供了一些公共的 Dispatch Queue,但是用户也可以自定义一些 dispatch queue。GCD 的出现是为了淡化用户对于线程的操作,我们仅需关心任务与任务之间的关系,它们是顺序执行还是并发执行。不需要太在意任务是在哪个线程中执行的。

iOS 对 dispatch queue 做了归类,分为三类:

  • Serial Dispatch Queue
  • Concurrent Dispatch Queue
  • Main Dispatch Queue

下面这张图,想必大家应该都不陌生吧:

gcd_pool

关于 GCD 的文章推荐 objc.io 这篇

Serial Dispatch Queue

Serial queues (also known as private dispatch queues) execute one task at a time in the order in which they are added to the queue. The currently executing task runs on a distinct thread (which can vary from task to task) that is managed by the dispatch queue. Serial queues are often used to synchronize access to a specific resource.

serial dispatch queue 中的任务是依照 FIFO 的顺序执行,实际上为单线程执行。即每次从 queue 中取出一个 task 进行处理;用户可以根据需要创建任意多的 serial dispatch queue,serial dispatch queue 彼此之间是并发的;

串行队列的创建方法如下,仅需注意参数:DISPATCH_QUEUE_SERIAL(即NULL)即可:

1dispatch_queue_t queue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL);

Concurrent Dispatch Queue

Concurrent queues (also known as a type of global dispatch queue) execute one or more tasks concurrently, but tasks are still started in the order in which they were added to the queue. The currently executing tasks run on distinct threads that are managed by the dispatch queue. The exact number of tasks executing at any given point is variable and depends on system conditions.

Concurrent Dispatch Queue 一次性并发执行一个或者多个 task;作为全局调度队列,GCD 提供 dispatch_get_global_queue 以获取不同优先级的并发队列。用户也可以根据需要自己定义 concurrent queue:

1dispatch_queue_t queue = dispatch_queue_create("com.example.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);

并发队列类似于其他语言里的线程池,其管理的 task 可能在多个不同 thread 上执行,至于 GCD 管理多少个 thread 是未知的,这要视系统资源而定。

Main Dispatch Queue

The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread.

main dispatch queue 是特殊的 serial dispatch queue,毕竟主线程只有一个。我们知道应用程序的主要任务 (例如 UI 操作)都在 main thread 中完成,而全局的 main disaptch queue 所调度的 task 都在 main thread 中运行。

所以,如果想要更新 UI,则必须在 main dispatch queue 中处理,获取 main dispatch queue 也很容易,调用 dispatch_get_main_queue() 函数即可。

RunLoop

在 iOS 中对于多线程问题是逃不开 RunLoop 的,不过 RunLoop 与 GCD 基本没半毛钱关系。

RunLoop 与线程才是一对好基友。

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

关于 RunLoop 应该没有比 深入理解RunLoop 文章说的更清楚的了,要不就自行看源码。

我们知道新开线程是需要开销的。如果执行一个任务,就开一次线程,执行完毕就释放了。当任务特别细碎,并且非常多时,这不断消费线程的开销会比处理这些任务的开销要大得多。RunLoop 就是为此而生的,有任务时唤醒线程,无任务的时候休眠但不退出,如此往复。

因此,没有 RunLoop, 线程可能会 “死”; 线程 “死”了,RunLoop 才会停止运行。

Dispatch Queues 的任务调度

在 GCD 中,dispatch_sync 用于派发同步任务,dispatch_async 用于派发异步任务。

dispatch_sync

先来一段代码:

1// dispatch task synchronously
2dispatch_sync(someQueue1, ^{
3    // do something 1
4});
5// do something 2

当执行到 dispatch_sync(...) 时,其上下文被阻塞,直到 dispatch_sync 派发的 block 被执行完毕。

正如上面代码,do something 2 一定会在 do something 1 完成之后执行,即所谓的同步

依据 官方描述

Submits a block to the specified dispatch queue for synchronous execution. Unlike dispatch_async, this function does not return until the block has finished. Calling this function and targeting the current queue results in deadlock.

Unlike with dispatch_async, no retain is performed on the target queue. Because calls to this function are synchronous, it “borrows” the reference of the caller. Moreover, no Block_copy is performed on the block.

As a performance optimization, this function executes blocks on the current thread whenever possible, with one obvious exception. Specifically, blocks submitted to the main dispatch queue always run on the main thread.

由于是同步等待任务,dispatch_sync 并不会对派发的 block 进行 copy 操作,block 内如果有使用外部变量也都会直接 借用 外部的,而不会对其引用进行 retain。

以性能优化的角度,dispatch_sync 派发的 block 会尽量在当前线程执行,如果我们在主队列中派发了 block,那 block 将会在主线程中执行。

结论 1:dispatch_sync 派发的 block 所执的行线程与 dispatch_sync 上下文线程是同一个线程

结论 2:dispatch_sync 是不需要拷贝 block 的,理由为结论 1

dispatch_async

先来一段代码:

1// dispatch task asynchronously
2dispatch_async(someQueue2, ^{
3    // do something 3
4});
5// do something 4

当执行到 dispatch_async(...) 时,其上下文不被阻塞,继续运行。正如上面的代码, do something 4 会立即执行,而不会等到 do something 3 执行完,即所谓异步

依据 官方描述

This function is the fundamental mechanism for submitting blocks to a dispatch queue. Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. The target queue determines whether the block is invoked serially or concurrently with respect to other blocks submitted to that same queue. Independent serial queues are processed concurrently with respect to each other.

结论 3:dispatch_async 派发的 block 所执行的线程和 dispatch_async 上下文线程不是同一个线程

Queues & Dispatch

队列与派发的组合如下:

dispatch_sync dispatch_async
serial queue 同步串行队列 异步串行队列
concurrent queue 同步并发队列 异步并发队列

Serial Queues Dispatch

来看一个示例:

 1// 1. create a serial dispatch queue
 2dispatch_queue_t serial_queue=
 3dispatch_queue_create("com.zhangbuhuai.test", DISPATCH_QUEUE_SERIAL);	// Thread main
 4
 5// 2. add tasks to serial dispatch queue
 6// 1) add a task synchronously
 7dispatch_sync(serial_queue, ^{
 8   sleep(3);	// 休眠3秒
 9   NSLog(@"task 1 %@", NSThread.currentThread);
10});
11// 2) add a task synchronously too
12dispatch_sync(serial_queue, ^{
13   NSLog(@"task 2 %@", NSThread.currentThread);
14});
15// 3) add a task asynchronously
16dispatch_async(serial_queue, ^{
17   NSLog(@"task 3 %@", NSThread.currentThread);
18});
19// 4) add a task asynchronously too
20dispatch_async(serial_queue, ^{
21   NSLog(@"task 4 %@", NSThread.currentThread);
22});
23
24NSLog(@"test end");         // Thread main

执行结果如下:

1task 1 <NSThread: 0x600002d04d00>{number = 1, name = main}
2task 2 <NSThread: 0x600002d04d00>{number = 1, name = main}
3test end
4task 3 <NSThread: 0x600002d452c0>{number = 4, name = (null)}
5task 4 <NSThread: 0x600002d452c0>{number = 4, name = (null)}

说明,对于 serial dispatch queue 中的 tasks,无论是同步派发还是异步派发,其执行顺序都遵循 FIFO;同样,这个示例也可以直观阐述 dispatch_syncdispatch_async 的不同效果。

结论 4:不存在所谓的 同步队列 和 异步队列

同步或异步描述的是 task 与其上下文之间的关系,因此,同步队列异步队列 对于 GCD 而言是不靠谱的概念。

结论 5:Serial Dispatch Queue 上的 tasks 并非只在同一个 thread 上执行

对于同步请求的任务,如果用 dispatch_sync 添加到 serial dispatch queue 中,其运行的 task 往往与所在的上下文是同一个 thread;

对于异步请求的任务,如果用 dispatch_async 添加到 serial dispatch queue 中,其运行的 task 往往是另一个的 thread。

总之,thread 和 dispatch queue 之间没有从属关系,也不存在一对一或者一对多的关系。

Concurrent Queue Dispatch

再来看一个示例:

1// 1. create a concurrent dispatch queue
2dispatch_queue_t serial_queue=
3dispatch_queue_create("com.zhangbuhuai.test", DISPATCH_QUEUE_CONCURRENT);    // Thread main
4
5// 2. add tasks to concurrent dispatch queue
6// 第二步逻辑与串行队列一致,此处不作展开 
7// ... 

其执行结果:

1task 1 <NSThread: 0x600000ff8d00>{number = 1, name = main}
2task 2 <NSThread: 0x600000ff8d00>{number = 1, name = main}
3test end
4task 4 <NSThread: 0x600000ff8c40>{number = 5, name = (null)}
5task 3 <NSThread: 0x600000ff2700>{number = 4, name = (null)}

说明,对于 dispatch_sync 中的 tasks,不论是在 CONCURRENT 或是 SERIAL 队列中执行,同步派发的 block 所执的行线程与 其上下文线程是同一个线程;同步派发的执行顺序遵循 FIFO;而异步派发的执行顺序就不一定了;

使用同步串行队列保护代码

最后,让我们回到引子中提到的 FMDatabaseQueue 的内部实现。它通过内部维护的 serial dispatch queue + dispatch_sync 来处理 inDatabase: 传入的 block,所以当我们调用 inDatabase: 时,代码实际上是同步执行的。

FMDatabaseQueue will run the blocks on a serialized queue (hence the name of the class). So if you call FMDatabaseQueue ’s methods from multiple threads at the same time, they will be executed in the order they are received. This way queries and updates won’t step on each other’s toes, and every one is happy.

FMDB 这么设计的目的是让我们避免发生并发访问数据库的问题,因为它无法保证用户访问数据库的时机。通过内置的同步串行队列后,FMDatabaseQueue 就变成线程安全了,所有的数据库访问都是同步执行,而且这比使用 @synchronized 或 NSLock 要高效得多。

不过,需要注意的是 FMDB 这么做了之后,我们在使用 FMDatabaseQueue 时候就得小心了。

不能嵌套调用 FMDatabaseQueue 否则线程死锁

参见如下代码:

1[queue inDatabase:^(FMDatabase *db) {
2    [db executeUpdate:...
3
4    [queue inDatabase:^(FMDatabase *db) {
5        [db executeUpdate:...
6    }]; 
7}];

由于是同步串行队列,嵌套调用会导致互相等待,造成死锁。这个也是我们常说到的少用 dispatch_sync 的主要原因。

个人看法:存在即合理,看似安全的方法使用不当也可能造成严重异常。反之,在合理的场景下使用合适的方法,如: dispatch_sync 反而有不错的效果,不能因噎废食。