Swift并行编程:操作

在Swift的并行编程:基础知识中,我介绍了许多低级选项来控制Swift中的并发。 最初的想法是在一个地方收集我们可以在iOS上使用的所有不同类型的方法。 但是在编写该指南时,我意识到有太多方法可以在一篇文章中列出。 因此,我决定减少高级方法。

我在上一篇文章中确实提到了操作,但是让我们更仔细地研究它们。

操作队列

让我们回顾一下:运营是Cocoa在GCD上的高级抽象。 准确地说,它是dispatch_queue_t的抽象。 它使用具有可在其中添加任务的队列的相同原理。 对于OperationQueue,这些任务是Operations。 在执行操作时,我们需要知道它正在运行的线程。 与往常一样,如果要更新UI,则需要MainOperationQueue。

操作在其生命周期中会经历不同的阶段。 当添加到队列中时,它处于挂起状态。 在这种状态下,它将等待其条件。 一旦所有这些都满足,它将进入就绪状态,并且如果有空闲插槽,它将开始执行。 完成所有工作后,它将进入完成状态,并将其从OperationQueue中删除。 在每种状态下(“完成”除外),均可取消操作。

消除

取消操作非常简单。 根据操作,取消可能具有完全不同的含义。 例如,运行网络请求,取消操作可能导致停止该请求。 在导入数据时,这可能意味着放弃您的交易。 您将负责给出取消的意思。

那么如何取消操作? 您只需调用.cancel()。 这将切换isCancelled属性。 这就是iOS为您服务的全部。 由您决定是否取消并相应地执行操作。

请注意,取消操作会导致它放弃所有条件,并立即开始立即执行以进入完成状态。 进入完成状态是将操作从队列中删除的唯一方法。

如果忘记检查取消标志,即使您取消了操作,也可能会看到正在执行的操作。 另请注意,这容易受到比赛条件的影响。 按下按钮并设置标志需要几微秒。 在此期间,操作可能会完成,并且取消标志不再起作用。

准备就绪

就绪状态仅由单个布尔标志来描述。 这意味着该操作已准备好执行,并且正在等待队列启动它。 在串行队列中,将首先执行进入就绪状态的操作,即使该操作可能位于队列中的位置9。 如果有多个操作同时进入“就绪”状态,则优先级将决定。 仅当一个操作的所有依赖项都完成时,它才会进入就绪状态。

依存关系

这是Operations真正重要的功能之一。 我们可以创建任务,具体说明其他任务必须先执行才能执行。 同时,有些任务可以与其他任务并行执行,但是是后续操作的依赖项。 这可以通过调用.addDependency()来完成。

默认情况下,任何具有依赖关系的操作都将在其所有依赖关系完成后才进入就绪状态。 但是,由您决定取消依赖项后如何继续。

这使我们能够严格命令我们的运营。

我认为这不容易阅读,因此让我们创建自己的运算符(==>)来创建依赖项。 这样,我们可以说,按从左到右的顺序执行操作。

关于依赖项的好处是,它们甚至可以跨不同的OperationQueue。 同时,这可能会导致意外的锁定行为。 例如,您的UI可能会结结巴巴,因为更新取决于后台操作并阻止其他操作。 注意循环依赖性。 如果操作A依赖于操作B并且B依赖于A就是这种情况。这样,它们都在等待另一个操作执行,因此您将创建死锁。

已完成

执行后,该操作将进入“完成”状态,并仅执行一次其完成块。 可以如下设置完成块:

实际例子

在所有这些基础知识基础上,让我们为Operations创建一个简单的框架。 操作具有很多复杂的概念。 与其创建一个过于复杂的示例,不如让我们仅打印“ Hello world”并尝试将其中的大多数内容合并在一起。 这将包含异步执行,依赖关系和被视为一个的多个操作。 让我们潜入吧!

异步操作

首先,我们将创建一个Operation以创建异步任务。 这样,我们可以继承并创建任何类型的异步任务。

这看起来很丑。 如您所见,我们必须重写isFinished和isExecuting。 此外,对这些更改的更改必须符合KVO,否则OperationQueue将无法观察我们的操作状态。 在start()方法中,我们管理从开始执行到进入完成状态的操作状态。 我们创建了一个方法execute()。 这将是我们的子类需要实现的方法。

文字操作

在这种情况下,我们只需要在init()中传递要打印的文本并覆盖execute()。

集团经营

GroupOperation将是我们的实现,用于将多个Operations合并为一个。

如您所见,我们创建一个数组,子类将在其中添加其操作。 然后在执行过程中,我们仅将操作添加到我们的私有队列中。 这样,我们确保它们将按顺序执行。 调用addOperations([Operation],waitUntilFinished:true)导致队列阻塞,直到完成添加的操作为止。 之后,GroupOperation将其状态更改为“完成”。

HelloWorldOperation

嘿,我们终于到了。 这真是小菜一碟! 只需创建您的操作,设置依赖项并将其添加到数组即可。 而已。

操作观察员

那么,我们如何找出一项操作已完成? 一种方法是添加一个competionBlock。 另一种是注册一个OperationObserver。 这是一个通过KVO在keyPaths上注册的类,只要您使其符合KVO,就可以观察所有内容。

在我们的小框架中,让我们在HelloWorldOperation完成后立即将其打印为“完成”:

传递数据

“ Hello World!”没有理由传递数据,但是让我们快速了解一下。 最简单的方法是使用BlockOperations。 使用这些,我们可以为下一个需要数据的操作设置属性。 不要忘记设置依赖项,否则,操作可能无法及时执行😉

错误处理

我们尚未看到的另一件事是错误处理。 实话说,我还没有找到一种很好的方法来做到这一点。 一种选择是添加一个完成方法(withErrors :),然后让每个AsyncOperation调用此方法,而不是AstartOperation在start()中对其进行处理。 这样,我们可以检查错误并将其添加到数组中。假设我们有一个依赖于操作B的操作A.突然之间,操作B会出现一些错误。 在这种情况下,操作A可以检查此数组并中止。 根据您的要求,您可以添加其他错误。

它可能看起来像这样:

请注意,子操作需要相应地处理其状态,并且需要在AsyncOperation上进行一些更改才能使其工作。

但是与往常一样,有很多方法,但这只是其中一种。 您也可以使用观察者来观察错误值。

你怎么做并不重要。 只要确保您的操作将自行清除即可。 例如:如果您写到CoreData上下文中而出现问题,则需要清除此上下文。 否则,您的状态可能会不一致。

UI操作

操作不仅限于看不见的元素。 您在应用程序中执行的所有操作都可以是“操作”(即使我建议您反对)。 但是有些事情,作为操作更容易看清。 模态的一切都应相应考虑。 让我们看一下显示对话框的操作:

如您所见,它将暂停执行直到按下按钮。 之后,它将进入完成状态,然后取决于该操作的所有其他操作可以继续。

互斥

考虑到我们可以使用Operations for UI,这带来了一个不同的问题。 想象一下在错误消息上显示一个对话框。 也许您将多个操作排队,当网络不可用时将显示错误。 这很容易导致所有这些操作创建显示连接问题的警报。 结果,我们将同时弹出多个对话框,并且不知道哪个是第一个或第二个。 因此,我们将不得不使这些对话框互斥。

尽管这个想法很复杂,但是很容易实现依赖关系。 只需在这些对话框之间创建依赖关系,即可完成。 一个问题是要跟踪操作。 但这可以通过命名操作,然后访问OperationQueue并搜索名称来解决。 这样,您不必保留引用。

结论

操作是一个很好的并发工具。 但是不要上当,它们比您想象的要难。 目前,我正在维护一个基于Operations的项目,其中某些部分纯属噩梦。 尤其是错误处理非常容易出错。 每次执行组操作失败时,都有可能发生多个错误。 您将不得不过滤掉相关错误,因此有时错误会被错误映射例程所掩盖。

另一个问题是您停止考虑可能的并发问题。 我还没有详细讨论这些内容,但是请注意上面带有错误处理代码的GroupOperations。 它包含一个错误,将在以后的文章中修复。 我不得不修复类似的问题,并且想向您展示由于您使用的工具而导致您不再考虑并发时将事情搞砸的容易程度。

如果发现它,请通过Twitter与我联系!

即使我这么说,Operations是控制并发的好工具。 GCD仍然没有超出预期。 对于需要尽快执行的诸如切换线程或任务之类的小任务,您可能不希望使用“操作”。 对于这些,GCD是完美的解决方案。