Swift并行编程—第1/4部分

并发,并行和调度队列

每当我们听到并行编程时,就会想到两个令人困惑的术语:并发和并行。 首先让我们看看现实生活中的角色:

并发性:我们的日常工作涉及多任务处理。 很多时候,我们执行多任务处理,但最终会同时完成一个任务,但是理想情况下,这就是上下文切换。 它也有一些限制。 假设我们每天有100项工作,但不能将其增加到1000项。

并行性:如果我们可以得到更多的物理东西,那么可以同时完成两个或更多的工作。 如果我有4臂,那么这篇文章可以在一半的时间内完成。

让我们来看看计算机世界中的两个术语:

并发:

并发意味着应用程序同时(并发)在一项以上的任务上取得进展。 如果计算机只有一个CPU,则该应用程序可能无法完全同时执行一项以上的任务,但是一次要在应用程序内部处理一项以上的任务。 在开始下一项任务之前,它并没有完全完成一项任务。

当我们谈论至少两个或更多任务时,并发本质上是适用的。 当一个应用程序几乎可以同时执行两个任务时,我们称其为并发应用程序。 尽管这里的任务看起来像是同时运行的,但实际上它们可能并非如此。 它们利用了操作系统的CPU时间分片功能,其中每个任务运行其任务的一部分,然后进入等待状态。

当第一个任务处于等待状态时,CPU被分配第二个任务以完成其部分任务。 操作系统根据任务的优先级工作,因此分配CPU和其他计算资源,例如内存; 依次处理所有任务,并给他们完成任务的机会。 对于最终用户,似乎所有任务都是并行运行的。

并发复杂度

假设有5个朋友搬进了房子,每个人都有一张床。 哪种更复杂的方式来构造它?

  • 5人同时组装一张床或
  • 每个人组装自己的床

想一想如何为几个朋友编写有关如何将床组装在一起的说明,而无需彼此扶持或等待工具。 他们需要在正确的时间协调动作,以将床的各个部分组装成成品床。 这些指令真的很复杂,很难编写,也可能很难阅读。

每个人都自己建造床时,说明非常简单,无需等待其他人完成操作或使用工具即可。

罗伯·派克Rob Pike)有一个关于 并发不是并行的演讲

平行性

并行不需要两个任务存在。 通过为每个任务或子任务分配一个内核,它可以使用CPU的多核基础结构同时运行部分任务或多个任务。 并行性本质上要求具有多个处理单元的硬件。 在单核CPU中,您可能会获得并发性,但不能获得并行性。

并发是独立执行的进程的组成,而并行性是计算的同时执行。 并行性是指应用程序将其任务分解为较小的子任务,这些子任务可以并行处理,例如在多个CPU上同时进行。

并发是一次处理很多事情,更多的是关注结构 并行性是一次执行很多事情,其重点是执行。

  • 一个应用程序可以是并发的,但不能是并行的,这意味着它可以同时处理一个以上的任务,但是两个任务不能同时执行。
  • 一个应用程序可以是并行的,但不能是并行的,这意味着它可以同时处理多核CPU中一个任务的多个子任务。
  • 应用程序既不能是并行的,也不能是并发的,这意味着它一次顺序地处理所有任务。
  • 一个应用程序可以是并行的,也可以是并发的,这意味着它可以同时在多核CPU中同时处理多个任务。

自从出现以来,CPU技术的最大改进之一就是能够包含多个内核,因此可以运行多个线程,这意味着在任何给定时刻可以执行多个任务。 在iOS中,有两种实现并发的方法:Grand Central Dispatch和OperationQueue。

这两个主题都很广泛,因此我将本文分为四个部分

并发和GCD — Swift并行编程— 1/4

GCD —使用Swift进行并行编程—第2/4部分

议程:

大中央派遣

  1. 同步和异步执行
  2. 串行和并发队列
  3. 系统提供的队列
  4. 自定义队列
  5. 参考文献

大中央派遣:

Grand Central Dispatch(GCD)是基于队列的API,允许按先进先出顺序在工作池上执行关闭。 完成顺序将取决于每个作业的持续时间。

分派队列可以串行或并发执行任务,但始终以FIFO顺序执行。 应用程序可以同步或异步地以块形式提交任务以排队。 调度队列在系统提供的线程池上执行此块。 无法保证将在哪个线程上执行任务。

GCD API在Swift 3中进行了一些更改,SE-0088对其设计进行了现代化改造,使其更加面向对象。

通过提交任务以分派系统管理的队列,该框架有助于在多核系统上同时执行代码。

同步和异步执行

每个任务或工作项都可以同步或异步执行。 如果是同步的,它将在开始下一个任务之前等待第一个任务完成,依此类推。 异步执行任务时,方法调用将立即返回,下一个任务将开始取得进展。

串行和并发队列

分派队列可以是串行的,以便一次执行一个工作项,也可以是并发的,以便按顺序使工作项出队,但可以一次全部运行并且可以按任何顺序完成。 串行队列和并发队列均以先进先出(FIFO)顺序处理工作项。

  • 串行队列

让我们看一个异步地将任务分配到主队列的示例。

由于此任务是异步执行的,因此最初执行当前队列上的for循环,然后执行调度队列中的任务,然后执行另一个任务。

在进行此实验时,我知道我们不能使用DispatchQueue.main.sync

尝试在主队列上同步执行工作项会导致死锁。

不要从传递给函数调用的同一队列中正在执行的任务中调用dispatch_sync函数。 这样做将死锁队列。 如果需要分派到当前队列,请使用dispatch_async函数异步进行。

— Apple文档

假设主线程是一个串行队列(这意味着它仅使用一个线程),则以下语句:

 DispatchQueue.main.sync {} 

将导致以下事件:

  1. sync将块放在主队列中。
  2. sync阻塞主队列的线程,直到该块完成执行。
  3. sync会永远等待,因为应该运行该块的线程被阻塞了。

理解这一点的关键是dispatch_sync不执行块,它仅将它们排队。 执行将在运行循环的将来迭代中发生。

作为一种优化,此函数在可能的情况下在当前线程上调用该块。

  • 并发队列:

可以通过将QoS传递给此方法来获取全局并发队列:

  DispatchQueue.global(qos:.default) 

系统提供的队列

启动应用程序时,系统会自动创建一个称为main queue的特殊队列 。 排队到主队列中的工作项在应用程序的主线程上顺序执行。 可以使用DispatchQueue.main访问主队列

除主队列外,系统还提供几个全局并发队列。 将任务发送到全局并发队列时,可以指定服务质量(QoS)类属性。

**主要QoS:

  • 与用户互动 :这表示必须立即完成的任务才能提供良好的用户体验。 使用它进行UI更新,事件处理和小的工作量。 在您的应用执行期间,此类中完成的工作总量应该很小。 这应该在主线程上运行。
  • 用户启动的 :用户从UI启动这些异步任务。 当用户等待即时结果以及继续用户交互所需的任务时,请使用它们。 它们在高优先级全局队列中执行。
  • 实用程序 :这表示长时间运行的任务。 使用它进行计算,I / O,联网,连续数据馈送和类似任务。 该类旨在提高能源效率。 实用程序任务通常具有用户可见的进度栏。 这将被映射到低优先级的全局队列中。 要执行的工作需要几秒钟到几分钟。
  • 背景 :这表示用户不直接知道的任务,例如预取,备份。 这将被映射到后台优先级全局队列中。 这对于需要花费大量时间(例如几分钟或几小时)的工作很有用。

**特殊QoS:

  • 默认值:此QoS的优先级介于用户启动和实用程序之间。 未分配QoS信息的工作被视为default ,并且GCD全局队列在此级别运行。
  • 未指定:表示缺少QoS信息,提示系统应推断环境QoS。 如果线程使用可能使线程退出QoS的旧版API,则它们可能具有未指定的QoS。

全局并发队列:

过去,GCD提供了高,默认,低和后台全局并发队列,以对工作进行优先级排序。 现在应使用相应的QoS类代替这些队列。

自定义队列

GCD提供3种类型的队列,即主队列,全局队列和自定义队列。

创建我们自己的队列时,可以使用三种初始化方法:

  • init(“ queueName”)
  • init(“ queueName”,属性:{attributes})
  • init(“ queueName”,qos:{QoS类},属性:{attributes},autoReleaseFrequency:{autoreleaseFrequency},目标:{queue})

第一个初始化程序将隐式创建一个串行队列。

第2和第3个初始化程序中提到的attributes指的是DispatchQueue.Attributes,这是一个具有两个选项的选项集: .concurrent (我们可以用来创建并发队列)和.initiallyInactive (允许我们创建非活动队列)。 可以修改不活动的队列,直到不活动的队列为止,并且直到通过调用activate()激活它们之前,它们才真正开始执行队列中的项目。 autoReleaseFrequency引用DispatchQueue.AutoreleaseFrequency。

.inherit:具有此自动释放频率的调度队列从其目标队列继承行为。 这是手动创建的队列的默认行为。

.workItem:使用此自动释放频率推送调度队列,并在异步提交给它的每个块的执行周围弹出一个自动释放池。 当队列使用每个工作项的自动释放频率(直接或从其目标队列继承)时,将异步地提交到此队列的任何块(通过async()、. barrier,.notify()等)执行,就像被包围一样通过一个单独的自动释放池。 自动释放频率对同步提交到队列(通过sync()、. barrier)的块没有影响。

.never:以这种自动释放频率调度的队列永远不会在异步提交给它的块执行周围设置单个自动释放池。 这是全局并发队列的行为。

让我们看看这些队列如何同步或异步工作:

  • 串行队列异步执行任务

在GCD中使用异步时,任务将在其他线程(主线程除外)中运行。 异步意味着执行下一行,不要等到块执行完毕才导致主线程和主队列不阻塞。 由于其串行队列,所有任务均按它们添加到串行队列的顺序执行。 串行添加的任务始终总是由与Queue关联的单个线程一次执行。

  • 串行队列同步执行任务

当您在GCD中使用同步时,任务可能会在主线程中运行。 Sync在给定队列上运行一个块,然后等待它完成,这将导致阻塞主线程或主队列。 由于主队列需要等待,直到调度的块完成,所以主线程将可用于处理除主队列之外的其他队列中的块。 因此,有可能在后台队列上执行的代码实际上可能在主线程上执行。由于它是串行队列,因此所有代码都按它们被添加的顺序执行(FIFO)。

  • 并发队列异步执行任务

当您在GCD中使用异步时,任务将在其他线程中运行。 异步意味着执行下一行,不要等到块执行完才导致非阻塞主线程。 与并发队列中一样,任务按照添加到队列中的顺序进行处理,但是队列中附加了不同的线程。 他们不应按照添加到队列中的顺序来完成任务。 每次由系统处理和分配线程时,任务顺序都会有所不同。 所有任务并行执行。

  • 并发队列同步执行任务

当您在GCD中使用同步时,任务可能会在主线程中运行。 Sync在给定队列上运行一个块,然后等待它完成,这将导致阻塞主线程或主队列。 由于主队列需要等待,直到调度的块完成,所以主线程将可用于处理除主队列以外的其他队列中的块。 因此,有可能在后台队列上执行的代码实际上可能在主线程上执行。 由于其并发队列,任务可能无法按它们添加到队列的顺序完成。 但是,尽管它们可以由不同的线程处理,但使用同步操作却可以。 因此,它的行为就像是串行队列。

asyncAfter:

如果要在延迟后在队列上执行任务,可以在asyncAfter()提供延迟,而不是使用sleep()