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

目标队列,调度组,屏障,工作项,信号量和调度源

本文是Swift并行编程的第2部分。 与第1部分一样,我们研究了Dispatch Queue和系统提供的队列。 在本文中,我将重点介绍另一种定义任务和GCD提供的强大API的方法。

如果要查看系列的所有部分:

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

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

议程:

  1. 目标队列
  2. 调度组
  3. DispatchWorkItem
  4. 派遣壁垒
  5. DispatchSemaphore
  6. 派遣源

1.目标队列

定制调度队列不执行任何工作,它只是将工作传递给目标队列。 默认情况下,自定义调度队列的目标队列是默认优先级的全局队列。 从Swift 3开始,一旦调度队列被激活,就无法再对其进行更改。 可以通过setTarget(queue:)函数设置自定义队列的目标队列。 在激活的队列上设置目标将进行编译,但随后会在运行时引发错误。 幸运的是,DispatchQueue初始化程序接受其他参数。 如果出于某种原因仍然需要将目标设置在已创建的队列上,则可以使用iOS 10以来可用的initiallyInactive属性来实现。

  DispatchQueue(标签:“队列”,属性:.initiallyInactive) 

这将允许您修改它,直到激活它。

DispatchQueue类的activate()方法将使任务执行。 由于队列尚未被标记为并发队列,因此它们将以串行顺序运行。 为了使队列并发,我们需要指定:

  DispatchQueue(标签:“队列”,属性:[。initiallyInactive,.concurrent]) 

您可以将任何其他调度队列(甚至是另一个自定义队列)传递给目标队列,只要您从未创建周期即可。 通过简单地将自定义队列的目标队列设置为其他全局队列,可以使用该功能设置自定义队列的优先级。 只有全局并发队列和主队列才能执行块。 所有其他队列必须(最终)以这些特殊队列之一为目标。

您可能已经在使用的Dispatch API或框架中注意到了目标队列。 我在RxSwift库中发现了目标队列的使用。

使用此实现,由于任务正在异步执行,因此很难在完成时获得通知。 这是DispatchGroup图片:

DispatchGroup允许工作的聚合同步。 它可以用于提交多个不同的工作项或块,并跟踪它们何时全部完成,即使它们可能在不同的队列中运行。 唯一必要的事情是在调度组上均衡地调用enter()leave() ,以使其同步我们的任务。 调用enter()以手动通知组任务已开始,并leave以通知工作已完成。 您也可以调用group.wait() ,它会阻塞当前线程,直到该组的任务完成为止。有两种方法来调用完成块:

  1. 使用wait()然后在主队列上执行完成块
  2. 呼叫组notify()
  3. wait(timeout:) 这将阻止当前线程,但是在指定的超时后,无论如何仍将继续。 要创建类型为DispatchTime的超时对象,语法.now() + 1将从现在开始创建一秒超时。
  4. wait(timeout:)返回一个枚举,可用于确定组是否已完成或超时。

其他一些用例:

  • 您需要运行两个不同的网络调用。 只有当它们都返回后,您才具有解析响应的必要数据。
  • 动画正在运行,与长时间的数据库调用并行。 这两个都完成后,您想隐藏一个加载微调器。
  • 您使用的网络API太快了。 现在,您虽然没有用到刷新手势,但似乎没有用。 API调用返回的速度如此之快,以至于刷新控件在完成外观动画后便立即退出自身,这使它看起来好像没有刷新。 为了解决这个问题,我们可以添加一个伪延迟。 也就是说,在隐藏刷新控件之前,我们可以等待一些最小时间阈值和网络调用。

2. DispatchWorkItem:

关于GCD的一个常见误解是“一旦安排了无法取消的任务,就需要使用 Operation API” 。 在iOS 8和macOS 10.10中引入了DispatchWorkItem ,它以易于使用的API提供了此确切功能。

DispatchWorkItem封装了可以执行的工作。 可以将工作项分派到DispatchQueueDispatchGroup 。 也可以将DispatchSource设置为DispatchSource事件,注册或取消处理程序。

换句话说, DispatchWorkItem封装了可以分派到任何队列的代码块。

调度工作项具有取消标志。 如果在运行之前将其取消,则调度队列将不会执行它,而是将其跳过。 如果在执行过程中取消了它,则cancel属性返回true。 在这种情况下,我们可以中止执行。 工作项也可以在任务完成时通知队列。

注意: GCD不会执行抢先取消。 要停止已经开始的工作项,您必须自己测试取消。

如果您不希望即时执行工作项(不是问题),则可以使用等待功能来帮助您。 我们可以为执行延迟传递一些时间间隔。

OOPS,DispatchWorkItem中有两种等待函数。 使用哪一个? 困惑?

func wait(timeout:DispatchTime)-> DispatchTimeoutResult

func wait(wallTimeout:DispatchWallTime)-> DispatchTimeoutResult

DispatchTime基本上是根据设备时钟的时间,如果设备进入睡眠状态,时钟也将进入睡眠状态。 完美的夫妻。

但是DispatchWallTime是根据挂钟显示的时间,他根本不睡觉,一个守夜人。

执行后,我们可以通过调用func notify(qos:DispatchQoS = default,标志:DispatchWorkItemFlags = default,队列:DispatchQueue,执行:@escaping()-> Void)来通知同一队列或其他队列

DispatchQoS类与DispatchQueue一起使用 ,它有助于根据任务的重要性对任务进行分类。 随着系统分配更多资源以加快执行速度,具有最高优先级的任务将首先执行。 但是优先级较低的任务将在以后执行,因为它需要较少的资源和能源。 它有助于使应用程序变得更灵敏,更节能。

DispatchWorkItemFlags基本上是一组可以自定义DispatchWorkItem行为的唯一选项。 根据您提供的标志值,它决定是创建新线程还是需要创建屏障。

2秒钟后,一个队列正在取消任务,这会中断图像下载任务,因为我们在for循环中执行isCancelled检查。

3. DispatchBarrier

单身人士经常担心的一个问题是,由于单身人士经常被同时访问单身实例的多个控制器使用,因此它们通常不是线程安全的。 可以从并发任务安全地调用线程安全代码,而不会引起数据损坏等任何问题。 非线程安全的代码一次只能在一个上下文中运行。

有两种线程安全情况需要考虑:在单例实例的初始化期间以及在对实例的读取和写入期间。

由于Swift如何初始化静态变量,因此初始化很容易。 首次访问静态变量时,它将初始化静态变量,并确保初始化是原子的。

关键部分是一段不能同时执行的代码,即不能同时从两个线程执行。 这通常是因为代码操纵了诸如变量之类的共享资源,如果该资源被并发进程访问,则该资源会损坏。

当提交到全局队列或未使用.concurrent属性创建的队列时,屏障块的行为与通过async()/ sync()API提交的*块相同。

让我们来一个问题:

在此示例中,多个线程试图同时操作并且由于该错误值而导致输出value变量。

这是一个竞赛条件或经典的读者编写器问题,其中许多队列上的块试图修改可变值。 结果,它在块中打印了错误的值。 Swift提供了一种优雅的解决方案,可以使用DispatchBarrier创建读/写锁定。

调度屏障使我们可以在并发调度队列中创建同步点。 在正常操作中,队列的行为就像正常的并发队列。 但是,当屏障执行时,它将充当串行队列。 屏障完成后,队列返回到正常的并发队列。

GCD会记录在屏障调用之前将哪些代码块提交给队列,并且当它们全部完成后,它将调用传入的屏障块。 同样,直到屏障块完成后,才会执行提交到队列的任何其他块。 但是,屏障调用将立即返回并异步执行此块。

从技术上讲,当我们向调度队列提交DispatchWorkItem或块时,我们设置了一个标志来指示它应该是在特定时间内在指定队列上执行的唯一项。 必须先完成在分发屏障之前提交到队列的所有项目,然后才能执行此DispatchWorkItem 。 当执行屏障时,它是唯一正在执行的任务,并且队列在此期间不执行任何其他任务。 屏障完成后,队列将返回其默认行为。

如果队列是串行队列或全局并发队列之一,则屏障将不起作用。 在自定义并发队列中使用屏障是处理关键代码区域中线程安全的好选择。

4. DispatchSemaphore

在多线程编程中,让线程等待很重要。 他们必须等待对资源的独占访问。 使线程等待并使其进入内核以使其不再占用任何CPU时间的一种方法是使用信号量 。 信号量由Dijkstra于1960年代初发明。

信号量使我们能够控制多个线程对共享资源的访问。 共享资源可以是变量,也可以是诸如从url下载图像,从数据库读取等任务。

信号量由线程队列和计数器值(Int类型)组成。

信号量使用线程队列以FIFO顺序跟踪正在等待的线程。

信号量使用计数器值来确定线程是否应该访问共享资源。 当我们调用signal()wait()函数时,计数器值发生变化。

请求共享资源:

每次使用共享资源之前,都要调用wait() 。 我们基本上是在询问信号量共享资源是否可用。 如果没有,我们将等待。

释放共享资源

每次使用共享资源后,都要调用signal() 。 我们基本上是在发出信号,表明我们已经完成了与共享资源的交互。

调用wait()执行以下工作:

  • 将信号量计数器减1。
  • 如果结果值小于零,则线程被阻塞,并将进入等待状态。
  • 如果结果值等于或大于零,则无需等待即可执行代码。

调用signal()执行以下工作:

  • 将信号量计数器增加1。
  • 如果先前的值小于零,则此函数取消阻塞当前在线程队列中等待的线程。
  • 如果先前的值等于或大于零,则意味着线程队列为空,没有人在等待。

下图显示了信号量的完美示例。

让我们在单个队列中实现信号量Swift

如果您在上面的代码中注意到,则首先在Semaphore上调用wait(),然后调用signal()。

让我们举一个在下载图像时使用信号量的示例。

现在,我们了解了信号量是如何工作的,下面我们来看一个对应用程序更实际的场景,即从URL下载6个图像。

首先,我们创建一个并发队列,该队列将用于执行图像下载代码块。

其次 ,我们创建一个信号量并将其设置为初始计数器值2,因为我们决定一次下载2个映像,以免一次占用太多CPU时间。

第三 ,我们使用for循环迭代6次。 在每次迭代中,我们执行以下操作: wait()→下载图像→signal()

让我们跟踪信号量计数器以获得更好的理解:

  • 2(我们的初始值)
  • 1(等待图像1,因为值> = 0,开始下载图像)
  • 0(等待图像2,因为值> = 0,开始下载图像)
  • -1(图像3等待,因为值<0,添加到队列)
  • -2(图像4等待,因为值<0,添加到队列中)
  • -3(图像5等待,因为值<0,添加到队列中)
  • -4(图像6等待,因为值<0,添加到队列中)
  • -3(图像1信号,最后一个值<0,唤醒图像3并将其从队列中弹出)
  • – 2(图像2信号,最后一个值<0,唤醒图像4并将其从队列中弹出)
  • -1(图像3信号,最后一个值<0,唤醒图像5并将其从队列中弹出)
  • 0(图像4信号,最后一个值<0,唤醒图像6并将其从队列中弹出)

从该序列中,您可以看到一个线程开始执行该序列时,另一个线程必须等待第一个线程结束。 第二个线程将在序列的哪一点发送wait()请求都无关紧要,它始终必须等待另一个线程完成。

最好仅在优先级相同的线程之间使用信号量,否则可能会导致优先级倒置问题。

派遣源

调度源是使用事件处理程序处理系统级异步事件(如内核信号或与系统,文件和套接字相关的事件)的便捷方法。

调度源可用于监视以下类型的系统事件:

  • 计时器调度源: 用于生成定期通知(DispatchSourceTimer)。
  • 信号调度源: 用于处理UNIX信号(DispatchSourceSignal)。
  • 内存调度源: 用于注册与内存使用状态有关的通知(DispatchSourceMemoryPressure)。
  • 描述符调度源: 描述符源发送与各种基于文件和套接字的操作相关的通知,例如:
  1. 当数据可供读取时发出信号;
  2. 在可以写入数据时发出信号;
  3. 文件删除,移动或重命名;
  4. 文件元信息更改。

(DispatchSourceFileSystemObject,DispatchSourceRead,DispatchSourceWrite)。

这使我们能够轻松构建具有“实时编辑”功能的开发人员工具。

  • 流程分派源: 用于监视外部流程以查找与其执行状态(DispatchSourceProcess)相关的某些事件。 与流程相关的事件,例如
  1. 流程退出;
  2. 进程发出fork或exec类型的调用;
  3. 信号传递到过程。
  • 与Mach相关的调度源: 用于处理与 Mach内核 IPC设施 有关的 事件 (DispatchSourceMachReceive,DispatchSourceMachSend)。
  • 我们触发的自定义事件:您可以通过遵循DispatchSourceProtocol来定义自定义调度源

让我们看一些用例:

  • NSTimer在需要主运行循环执行的主线程上运行。 如果要在后台线程上执行NSTimer,则不能。 在这种情况下,可以使用DispatchSourceTimer 。 在时间间隔完成后,调度计时器源将触发一个事件,然后在同一队列上均触发一个预设的回调。 阅读更多…。
  • 如果您必须监视文件中的更改,则可以使用DispatchSource Descriptor轻松实现。

注意:

一旦调度任务开始运行,取消或挂起任务/队列/工作项都不会停止它。 取消和挂起操作只会影响尚未调用的任务。 如果必须执行此操作,则将需要在任务执行期间的适当时候手动检查已取消状态,并在需要时退出任务。