Swift 并发任务是否按顺序运行?

问题描述 投票:0回答:1

我无法找到讨论任务是否同时运行的文档。或者,如果任务按顺序运行,则在某个不可见的队列中。

以下是我的应用程序遇到的一个精简问题,它可以在游乐场中运行,这引发了这个问题。

import UIKit
import Foundation
import Combine

struct Info {
    var id: String
    var value: Int
}

class DataStore {
        // pretend this is storing into core data
    func store(info: Info, id: String) {
        print("    store \(info)")
        let start = CACurrentMediaTime()
        while CACurrentMediaTime() - start < 2 { }  
    }
}

let dataStore = DataStore()
let subj = PassthroughSubject<Info, Never>()
let cancel = subj.sink { info in
    print("Start task for \(info)")
    // is there a way to queue tasks so that we
    Task {
        print("  start \(info)")
        dataStore.store(info: info, id: info.id)
        print("  finish: \(info)")
    }
}

subj.send(Info(id: "A", value: 1))
subj.send(Info(id: "A", value: 2))
subj.send(Info(id: "A", value: 3))
subj.send(Info(id: "A", value: 4))

let queueA = DispatchQueue(label: "A", attributes: .concurrent)
let queueB = DispatchQueue(label: "B", attributes: .concurrent)

queueA.async {
    subj.send(Info(id: "A", value: 1))
    subj.send(Info(id: "A", value: 2))
    subj.send(Info(id: "A", value: 3))
    subj.send(Info(id: "A", value: 4))
}

queueB.async {
    subj.send(Info(id: "B", value: 1))
    subj.send(Info(id: "B", value: 2))
    subj.send(Info(id: "B", value: 3))
    subj.send(Info(id: "B", value: 4))
}

queueA.async {
    subj.send(Info(id: "A", value: 1))
    subj.send(Info(id: "A", value: 2))
    subj.send(Info(id: "A", value: 3))
    subj.send(Info(id: "A", value: 4))
}

queueB.async {
    subj.send(Info(id: "B", value: 1))
    subj.send(Info(id: "B", value: 2))
    subj.send(Info(id: "B", value: 3))
    subj.send(Info(id: "B", value: 4))
}

// Note that a closure is not started until the other one has finished

请注意,在前一个闭包完成之前,闭包永远不会开始。现在我不知道这是因为传递主题是让事情按顺序进行还是与发布者有其他关系。

我知道这不是一个完美的例子,因为发布者的原因,但我的应用程序有旧的组合代码与新的异步等待代码接口。

附注如果我使用异步序列而不是发布者,会有什么不同吗?

swift async-await concurrency task
1个回答
4
投票

您询问异步等待任务是否按顺序运行。答案是“视情况而定”。

A

Task
“[代表当前参与者异步运行]给定的...操作,作为新的顶级任务的一部分。”正如《Swift 编程语言:并发》中所说,“参与者一次只允许一个任务访问其可变状态”。因此,简而言之,如果您的异步等待任务位于 Actor 上,则可以阻止并行执行。 为了说明这一点,我将在 Instruments 中分析该应用程序(使用

os_signpost

事件)并使任务执行速度稍慢一些(以便我们可以轻松查看发生了什么)。

无论如何,请考虑这个 SwiftUI 示例,其中 

Task

与任何特定参与者隔离: import SwiftUI import os.log let poi = OSSignposter(subsystem: "Test", category: .pointsOfInterest) struct ContentView: View { var body: some View { VStack { Button("Launch Ten Tasks") { launchTenTasks() } } .padding() } func launchTenTasks() { let experiment = Experiment() for i in 0 ..< 10 { Task { experiment.spin(index: i, for: .seconds(1)) } } } } class Experiment { func spin(index: Int, for duration: Duration) { let id = poi.makeSignpostID() poi.withIntervalSignpost(#function, id: id, "\(index)") { let start = ContinuousClock().now while start.duration(to: .now) < duration { } // blocking thread is bad idea, but just simulating some slow, synchronous task } } }

结果:

您可以看到任务正在并行运行。

现在这有点例外。这只是证明单独使用

Task

不一定足以避免并行执行。

但我们经常将任务与演员结合使用。在这种情况下,您

do

实现了非并行执行。 因此,如果我将

Experiment

设为 actor,则这些方法将是 actor 隔离的,因此不会并行运行。现在,可以使用

@MainActor
限定符让它在主 actor 上运行(这在本例中会很糟糕,因为它会阻塞主线程),或者如下所示,将其设为一个单独的 actor:
actor Experiment {
    func spin(index: Int, for interval: TimeInterval) {
        ...
    }
}

产量:

我想提请您注意这样一个事实:这实现了串行行为,但这不是

队列。请注意,第 8 项和第 9 项没有顺序。 (它们主要按顺序处理,但并非严格如此。)如果您想要类似队列的行为,其中项目以严格的 FIFO 方式处理,您可以使用

AsyncSequence,例如

AsyncStream
AsyncChannel
。但这可能超出了这个问题的范围。

在你的例子中,如果你的

DataStore
是一个演员,那会阻止并发执行。而且,FWIW,

store

 不应该被标记为 
async
 方法,因为它不执行任何异步操作(至少在这一点上)。
关于您的示例的其他一些注意事项:

您的任务是如此之快,以至于很难看出它是否真的是顺序运行还是并行运行:请注意,我使用人为较慢的示例执行测试,以准确地清楚地表明并行执行与非并发执行。
  1. 您在游乐场中进行了测试:恕我直言,游乐场并不是测试并发性的良好环境。您希望在一个测试应用程序中运行它,该应用程序在与最终目标平台并行的环境中运行。游乐场具有各种特殊的行为,从中很容易得出错误的结论。
  2. 我建议不要将Combine 和(尤其是)GCD 与异步等待代码混合使用。
  3. 因此,在真实的应用程序中运行它,如果您想确保它不会并行运行,请确保您的方法与参与者隔离。

© www.soinside.com 2019 - 2024. All rights reserved.