iOS 并发编程之 Operation Queues

392 查看

现如今移动设备也早已经进入了多核心 CPU 时代,并且随着时间的推移,CPU 的核心数只会增加不会减少。而作为软件开发者,我们需要做的就是尽可能地提高应用的并发性,来充分利用这些多核心 CPU 的性能。在 iOS 开发中,我们主要可以通过 Operation Queues、Dispatch Queues 和 Dispatch Sources 来提高应用的并发性。本文将主要介绍 Operation Queues 的相关知识,另外两个属于 Grand Central Dispatch(以下正文简称 GCD )的范畴,将会在后续的文章中进行介绍。

由于本文涉及的内容较多,所以建议读者先提前了解一下本文的目录结构,以便对本文有一个宏观的认识:

  • 基本概念
    • 术语
    • 串行 vs. 并发
    • 同步 vs. 异步
    • 队列 vs. 线程
  • iOS 的并发编程模型
  • Operation Queues vs. Grand Central Dispatch (GCD)
  • 关于 Operation 对象
    • 并发 vs. 非并发 Operation
    • 创建 NSInvocationOperation 对象
    • 创建 NSBlockOperation 对象
  • 自定义 Operation 对象
    • 执行主任务
    • 响应取消事件
    • 配置并发执行的 Operation
    • 维护 KVO 通知
  • 定制 Operation 对象的执行行为
    • 配置依赖关系
    • 修改 Operation 在队列中的优先级
    • 修改 Operation 执行任务线程的优先级
    • 设置 Completion Block
  • 执行 Operation 对象
    • 添加 Operation 到 Operation Queue 中
    • 手动执行 Operation
    • 取消 Operation
    • 等待 Operation 执行完成
    • 暂停和恢复 Operation Queue
  • 总结

基本概念

在正式开始介绍 Operation Queues 的相关知识前,我想先介绍几个在 iOS 并发编程中非常容易混淆的基本概念,以帮助读者更好地理解本文。,本文中的 Operation Queues 指的是 NSOperation 和 NSOperationQueue 的统称。

术语

首先,我们先来了解一下在 iOS 并发编程中非常重要的三个术语,这是我们理解 iOS 并发编程的基础:

  • 进程(process),指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了;
  • 线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
  • 任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。

串行 vs. 并发

从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。

同步 vs. 异步

同样的,同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。同步操作会等待操作执行完成后再继续执行接下来的代码,而异步操作则恰好相反,它会在调用后立即返回,不会等待操作的执行结果。

队列 vs. 线程

有一些对 iOS 并发编程模型不太了解的同学可能会对队列和线程产生混淆,不清楚它们之间的区别与联系,因此,我觉得非常有必要在这里简单地介绍一下。在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。

iOS 的并发编程模型

在其他许多语言中,为了提高应用的并发性,我们往往需要自行创建一个或多个额外的线程,并且手动地管理这些线程的生命周期,这本身就已经是一项非常具有挑战性的任务了。此外,对于一个应用来说,最优的线程个数会随着系统当前的负载和低层硬件的情况发生动态变化。因此,一个单独的应用想要实现一套正确的多线程解决方案就变成了一件几乎不可能完成的事情。而更糟糕的是,线程的同步机制大幅度地增加了应用的复杂性,并且还存在着不一定能够提高应用性能的风险。

然而,值得庆幸的是,在 iOS 中,苹果采用了一种比传统的基于线程的系统更加异步的方式来执行并发任务。与直接创建线程的方式不同,我们只需定义好要调度的任务,然后让系统帮我们去执行这些任务就可以了。我们可以完全不需要关心线程的创建与销毁、以及多线程之间的同步等问题,苹果已经在系统层面帮我们处理好了,并且比我们手动地管理这些线程要高效得多。

因此,我们应该要听从苹果的劝告,珍爱生命,远离线程。不过话又说回来,尽管队列是执行并发任务的首先方式,但是毕竟它们也不是什么万能的灵丹妙药。所以,在以下三种场景下,我们还是应该直接使用线程的:

  • 用线程以外的其他任何方式都不能实现我们的特定任务;
  • 必须实时执行一个任务。因为虽然队列会尽可能快地执行我们提交的任务,但是并不能保证实时性;
  • 你需要对在后台执行的任务有更多的可预测行为。

Operation Queues vs. Grand Central Dispatch (GCD)

简单来说,GCD 是苹果基于 C 语言开发的,一个用于多核编程的解决方案,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。而 Operation Queues 则是一个建立在 GCD 的基础之上的,面向对象的解决方案。它使用起来比 GCD 更加灵活,功能也更加强大。下面简单地介绍了 Operation Queues 和 GCD 各自的使用场景:

  • Operation Queues :相对 GCD 来说,使用 Operation Queues 会增加一点点额外的开销,但是我们却换来了非常强大的灵活性和功能,我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 等;
  • GCD :则是一种更轻量级的,以 FIFO 的顺序执行并发任务的方式,使用 GCD 时我们并不关心任务的调度情况,而让系统帮我们自动处理。但是 GCD 的短板也是非常明显的,比如我们想要给任务之间添加依赖关系、取消或者暂停一个正在执行的任务时就会变得非常棘手。

关于 Operation 对象

在 iOS 开发中,我们可以使用 NSOperation 类来封装需要执行的任务,而一个 operation 对象(以下正文简称 operation )指的就是 NSOperation 类的一个具体实例。NSOperation 本身是一个抽象类,不能直接实例化,因此,如果我们想要使用它来执行具体任务的话,就必须创建自己的子类或者使用系统预定义的两个子类,NSInvocationOperation 和 NSBlockOperation 。

NSInvocationOperation :我们可以通过一个 objectselector 非常方便地创建一个 NSInvocationOperation ,这是一种非常动态和灵活的方式。假设我们已经有了一个现成的方法,这个方法中的代码正好就是我们需要执行的任务,那么我们就可以在不修改任何现有代码的情况下,通过方法所在的对象和这个现有方法直接创建一个 NSInvocationOperation 。

NSBlockOperation :我们可以使用 NSBlockOperation 来并发执行一个或多个 block ,只有当一个 NSBlockOperation 所关联的所有 block 都执行完毕时,这个 NSBlockOperation 才算执行完成,有点类似于 dispatch_group 的概念。

另外,所有的 operation 都支持以下特性:

  • 支持在 operation 之间建立依赖关系,只有当一个 operation 所依赖的所有 operation 都执行完成时,这个 operation 才能开始执行;
  • 支持一个可选的 completion block ,这个 block 将会在 operation 的主任务执行完成时被调用;
  • 支持通过 KVO 来观察 operation 执行状态的变化;
  • 支持设置执行的优先级,从而影响 operation 之间的相对执行顺序;
  • 支持取消操作,可以允许我们停止正在执行的 operation 。

并发 vs. 非并发 Operation

通常来说,我们都是通过将 operation 添加到一个 operation queue 的方式来执行 operation 的,然而这并不是必须的。我们也可以直接通过调用 start 方法来执行一个 operation ,但是这种方式并不能保证 operation 是异步执行的。NSOperation 类的 isConcurrent 方法的返回值标识了一个 operation 相对于调用它的 start 方法的线程来说是否是异步执行的。在默认情况下,isConcurrent 方法的返回值是 NO ,也就是说会阻塞调用它的 start 方法的线程。

如果我们想要自定义一个并发执行的 operation ,那么我们就必须要编写一些额外的代码来让这个 operation 异步执行。比如,为这个 operation 创建新的线程、调用系统的异步方法或者其他任何方式来确保 start 方法在开始执行任务后立即返回。

在绝大多数情况下,我们都不需要去实现一个并发的 operation 。如果我们一直是通过将 operation 添加到 operation queue 的方式来执行 operation 的话,我们就完全没有必要去实现一个并发的 operation 。因为,当我们将一个非并发的 operation 添加到 operation queue 后,operation queue 会自动为这个 operation 创建一个线程。因此,只有当我们需要手动地执行一个 operation ,又想让它异步执行时,我们才有必要去实现一个并发的 operation 。

创建 NSInvocationOperation 对象

正如上面提到的,NSInvocationOperation 是 NSOperation 类的一个子类,当一个 NSInvocationOperation 开始执行时,它会调用我们指定的 objectselector 方法。通过使用 NSInvocationOperation 类,我们可以避免为每一个任务都创建一个自定义的子类,特别是当我们在修改一个已经存在的应用,并且这个应用中已经有了我们需要执行的任务所对应的 objectselector 时非常有用。

下面的示例代码展示了如何通过 objectselector 创建一个 NSInvocationOperation 对象。说明,本文中的所有示例代码都可以在这里 OperationQueues 找到,每一个类都有与之对应的测试类,充当 client 的角色,建议你在看完一个小节的代码时,运行一下相应的测试用例,观察打印的结果,以加深理解。

另外,我们在前面也提到了,NSInvocationOperation 类的使用可以非常的动态和灵活,其中比较显著的一点就是我们可以根据上下文动态地调用 object 的不同 selector 。比如说,我们可以根据用户的输入动态地执行不同的 selector

创建 NSBlockOperation 对象

NSBlockOperation 是 NSOperation 类的另外一个系统预定义的子类,我们可以用它来封装一个或多个 block 。我们知道 GCD 主要就是用来进行 block 调度的,那为什么我们还需要 NSBlockOperation 类呢?一般来说,有以下两个场景我们会优先使用 NSBlockOperation 类:

  • 当我们在应用中已经使用了 Operation Queues 且不想创建 Dispatch Queues 时,NSBlockOperation 类可以为我们的应用提供一个面向对象的封装;
  • 我们需要用到 Dispatch Queues 不具备的功能时,比如需要设置 operation 之间的依赖关系、使用 KVO 观察 operation 的状态变化等。

下面的示例代码展示了创建一个 NSBlockOperation 对象的基本方法: