Multiple Threading with Swift

借着 Swift 重新整理下 iOS 中的多线程用法,像GCD。

Mac OS 和 iOS 采取“异步 设计方式”来解决并发的问题。引入的异步技术有两个:

  • Operation Queue Objective-C 对象,类似于 dispatch queue。你 定义想要执行的任务,并添加任务到 operation queue,后者负责 调度和执行这些任务。和 GCD 一样,Operation Queue 也管理了 线程,更加高效。
  • Grand Central Dispatch 系统管理线程,你不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的 dispatch queue。GCD 会负责创建线程和调度你的任务。系统直接提供线 程管理,比应用实现更加高效。

Operation vs GCD

libdispatch 是Apple 的并发编程的库,GCD 是我们常听到的别名。在iOS 4 时被引入,它是新的更高层次的抽象。本文也将主要介绍它的使用。

GCD 有两种形式,Operation 是其对象化的版本。

  • GCD可以将计算复杂的任务放到后台执行,从而提升app的响应性能。
    – GCD提供了比 NSThreadNSOperation 更简单的并发模型,帮助开发者避免并发的bug。

  • 用NSOperation代替GCD

线程和并发

Serial vs. Concurrent

这两个词用来描述任务的执行顺序。

  • 串行 在同一时间点总是单独执行一个任务
  • 并发 可以同时执行多个任务

Synchronous vs. Asynchronous

这两个词描述的是函数何时将控制权返回给调用者,以及在返回时任务的完成情况。

  • 同步函数只有在任务完成后才会返回。
  • 异步函数会立即返回,不会等待任务完成。因此异步函数不会阻塞当前线程。

Concurrency vs Parallelism

并发和并行经常会被同时提起,所以值得通过简短的解释来区分彼此。

  • 并发代码中的单独部分可以同时执行。然而,这要由系统来决定并发怎样发生或是否发生。
  • 多核设备通过并行来同时执行多个线程;然而,在单核设备中,必须要通过上下文切换来运行另一个线程或进程。这一过程通常发生的很快以至于给人并行的假象。

GCD (Grand Central Dispatch)

GCD中的FIFO队列称为dispatch queue,它可以保证先进来的任务先得到执行。

GCD中有两个核心概念,一是任务,二是队列。

  • 任务:要执行什么样的操作。任务都是预先以Block封装好准备要执行的一段代码。
  • 队列:用来存放任务,按照先进先出的方式,调度任务在哪一条线程上执行。

Dispatch Queues 调度队列

Dispatch Queue 是用来执行任务的队列,是GCD中最基本的元素之一。

Dispatch Queue 是类似于对象的结构体,管理你提交给它的任务,而且都是 先进先出的数据结构。因此 queue 中的任务总是以添加的顺序开始执行,队列中总是顺序执行的,一个接一个。。GCD 提供了几种 dispatch queues,不过你也自己创建。

  • 串行队列 Serial Dispatch Queue, (也称 private dispatch queue),同时只执行一个任务。Serial queue 通常用于同步访问特定的资源或数据。当你创建多个Serial queue 时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。
  • 并行 Concurrent Queue,(也称 global dispatch queue),可以并发地执行多个任务,但是执行完成的顺序是随机的。
  • Main Dispatch Queue
    它是全局可用的 Serial queue,它是在应用程序主线程上执行任务的。

除了 Dispatch Queue,GCD还提供了

  • Dispatch Group
  • Dispatch Semaphore
  • Dispatch Source

GCD 系统队列

首先,系统提供了一种特殊的串行队列 Main Queue。和其他的串行队列一样,在这个队列里的任务同一时刻只有一个在执行。然而,这个队列保证了所有任务会在主线程中执行,主线程是唯一一个允许更新UI的线程。这个队列用来向 UIView 对象发消息或发通知。

系统同时提供了4种并发队列 Concurrent Queue。这些队列和它们自身的QoS等级相关。QoS等级表示了提交任务的意图,使得GCD可以决定如何制定优先级。

  • QOS_CLASS_USER_INTERACTIVE // Quick & high priority user interactive 等级表示任务需要被立即执行以提供好的用户体验。使用它来更新UI,响应事件以及需要低延时的小工作量任务。这个等级的工作总量应该保持较小规模。
  • QOS_CLASS_USER_INITIATED // High priority, might take time
  • QOS_CLASS_UTILITY // Long running
  • QOS_CLASS_BACKGROUND // User not concerned with this (prefetching, etc.)

除了这系统提供的Main Queue串行对列和4种并发队列,我们也可创建自定义的Global Dispatch Queue

iOS 7.0 之前 优先级 对应表

  • DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
  • DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND

Main Queue

  • 所有的UI事件,都必须发生成main queue。
  • 所有的time-consuming stuff 都不能执行在main queue中。

在ViewController 生命周期中的一件方法,像view did load 和自定义方法,都会默认在main queue执行,不用特意声明。

1
2
3
4
// 回到主队列
let main_queue: dispatch_queue_t = dispatch_get_main_queue()
// 或者使用对象式的API
let main_queue: NSOperationQueue = NSOperationQueue.mainQueue()

创建管理 Dispatch Queue

1. 以上是通过手动创建的方式来获取Dispatch Queue

1
dispatch_queue_t q = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

参数:

  • const char *label:队列的名称,一般是使用倒序的全域名。有名称的队列方便调试
  • dispatch_queue_attr_t attr:队列的属性,属性有两个,分别为:
    • DISPATCH_QUEUE_SERIAL(NULL) 串行队列
    • DISPATCH_QUEUE_CONCURRENT 并发队列
1
let myQueue: dispatch_queue_t = dispatch_queue_create("com.xxx", nil)

直接获取系统提供的Dispatch Queue。

要获取的Dispatch Queue无非就是两种类型:

  • Main Dispatch Queue
  • Global Dispatch Queue / Concurrent Dispatch Queue

一般只在需要更新UI时我们才获取Main Dispatch Queue,其他情况下用Global Dispatch Queue就满足需求了:

1
2
3
4
5
6
7
8
9
10
// 获取Main Dispatch Queue
let mainQueue = dispatch_get_main_queue()
// 获取Global Dispatch Queue 方式一
let qos = Int(QOS_CLASS_UTILITY.value)
let queue = dispatch_get_global_queue(qos, 0)
// 获取Global Dispatch Queue 方式二
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

得到的Global Dispatch Queue实际上是一个Concurrent Dispatch Queue,Main Dispatch Queue实际上就是Serial Dispatch Queue(并且只有一个)。 获取Global Dispatch Queue的时候可以指定优先级,可以根据自己的实际情况来决定使用哪种优先级。 一般情况下,我们通过第二种方式获取Dispatch Queue就行了。

执行任务

GCD中有两个用来执行任务的函数,分别是同步函数和异步函数

- 同步函数 dispatch_sync

1
2
dispatch_sync(dispatch_queue_t queue, ^(void)block)
// 其中queue为队列,block为任务

同步任务并不会创建线程,但会在当前线程(可以是子线程,页可以是主线程)中执行,同步任务有一个特性,只要同步任务一添加到队列中就要马上执行,同步任务不执行完就不会执行往后的代码。

- 异步函数 dispatch_async

1
dispatch_async(dispatch_queue_t queue, ^(void)block)

一般情况下异步任务都会开启一条子线程在后台执行(有一种情况除外,后面会讲到),异步任务的一个特性就是不用等待当前线程的任务就能直接执行

1
2
3
4
5
6
7
8
9
let qos = Int(QOS_CLASS_UTILITY.value)
let globalQueue = dispatch_get_global_queue(qos, 0)
dispatch_async(globalQueue) {
// do something that might block or takes a while
dispatch_async(dispatch_get_main_queue()) {
// call UI functions with the results of the above
}
}

队列开启多少条我们无法控制,异步函数并不会等待当前线程(当前线程为主线程)的任务。

- 延时任务 dispatch_after

延时任务

- 调度组 dispatch_group

多个异步操作都执行完毕后才回到主线程执行操作。使用调度组。

调度组的创建代码如下:

1
dispatch_group_t group = dispatch_group_create();

往调度组里面添加任务的函数如下:

1
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
// 添加任务
// group 负责监控任务,queue 负责调度任务
dispatch_group_async(group, q, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"任务1 %@", [NSThread currentThread]);
});
dispatch_group_async(group, q, ^{
NSLog(@"任务2 %@", [NSThread currentThread]);
});
// 监听所有任务完成 - 等到 group 中的所有任务执行完毕后,"由队列调度 block 中的任务异步执行!"
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 修改为主队列,后台批量下载,结束后,主线程统一更新UI
NSLog(@"OK %@", [NSThread currentThread]);
});

其它

  • dispatch_barrier_async
  • dispatch_apply
  • dispatch_suspend / dispatch_resume
  • Dispatch Semaphore
  • dispatch_once

Closure

Execute a function on another queue

1
2
let queue: dispatch_queue_t = <>
dispatch_async(queue) { /* */ }

REF::