并发执行代码中捕获的 var 的突变

Jer*_*ere 20 async-await swift xcode14

我在 Swift 5.5 中遇到了问题,但我不太明白解决方案。

import Foundation

func testAsync() async {

    var animal = "Dog"

    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        animal = "Cat"
        print(animal)
    }

    print(animal)
}

Task {
    await testAsync()
}
Run Code Online (Sandbox Code Playgroud)

这段代码会导致错误

Mutation of captured var 'animal' in concurrently-executing code
Run Code Online (Sandbox Code Playgroud)

但是,如果将animal变量移离此异步函数的上下文,

import Foundation

var animal = "Dog"

func testAsync() async {
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        animal = "Cat"
        print(animal)
    }

    print(animal)
}

Task {
    await testAsync()
}
Run Code Online (Sandbox Code Playgroud)

它会编译。我知道这个错误是为了防止数据竞争,但为什么移动变量会使其安全?

Rob*_*Rob 16

关于全局变量示例的行为,我可能会建议您参考Rob Napier\xe2\x80\x99s与全局变量可发送性相关的错误/限制的评论:

\n
\n

编译器在如何推理全局变量方面有很多限制。简短的答案是\xe2\x80\x9cdon\不创建全局可变变量。\xe2\x80\x9d它\xe2\x80\x98s出现在论坛上,但\xe2\x80\x98没有得到任何讨论。https://forums.swift.org/t/sendability-checking-for-global-variables/56515

\n
\n

FWIW,如果您将其放入实际的应用程序中并将 \xe2\x80\x9cStrict Concurrency Checking\xe2\x80\x9d 构建设置更改为 \xe2\x80\x9cComplete\xe2\x80\x9d,您确实会在全局示例:

\n
\n

对 var \'animal\' 的引用不是并发安全的,因为它涉及共享可变状态

\n
\n

这种对线程安全问题的编译时检测正在不断发展,Swift 6 中承诺出现许多新错误(这就是为什么他们\xe2\x80\x99 给了我们这个新的 \xe2\x80\x9c 严格并发检查\xe2\x80\x9d设置,以便我们可以开始使用不同级别的检查来检查我们的代码)。

\n

无论如何,您可以使用参与者来提供与此值的线程安全交互:

\n
actor AnimalActor {\n    var animal = "Dog"\n    \n    func setAnimal(newAnimal: String) {\n        animal = newAnimal\n    }\n}\n\nfunc testAsync() async {\n    let animalActor = AnimalActor()\n    \n    Task {\n        try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)\n        await animalActor.setAnimal(newAnimal: "Cat")\n        print(await animalActor.animal)\n    }\n\n    print(await animalActor.animal)\n}\n\nTask {\n    await testAsync()\n}\n
Run Code Online (Sandbox Code Playgroud)\n

有关更多信息,请参阅 WWDC 2021\xe2\x80\x99s使用 Swift Actor 保护可变状态和 2022\xe2\x80\x99s使用 Swift Concurrency 消除数据竞争

\n
\n

请注意,在上面,我避免使用 GCD API。这asyncAfter是古老的 GCD 技术,用于推迟某些工作而不阻塞当前线程。但新的Task.sleep(与旧的不同Thread.sleep)在并发系统中实现了相同的行为(并提供取消功能)。如果可能,我们应该避免在 Swift 并发代码库中使用 GCD API。

\n


Ber*_*rik 7

首先,如果可以的话,请使用结构化并发,正如其他答案所建议的那样。

我遇到了一个没有干净的结构化并发 API 的情况:一个需要非异步返回值的协议。

protocol Proto {
    func notAsync() -> Value
}
Run Code Online (Sandbox Code Playgroud)

要计算值,需要异步方法调用。我选择了这个解决方案:

func someAsyncFunc() async -> Value {
    ...
}

class Impl: Proto {
    func notAsync() -> Value {
        return UnsafeTask {
            await someAsyncFunc()
        }.get()
    }
} 

class UnsafeTask<T> {
    let semaphore = DispatchSemaphore(value: 0)
    private var result: T?
    init(block: @escaping () async -> T) {
        Task {
            result = await block()
            semaphore.signal()
        }
    }

    func get() -> T {
        if let result = result { return result }
        semaphore.wait()
        return result!
    }
}
Run Code Online (Sandbox Code Playgroud)

如果遇到相同的情况,您可以复制 UnsafeTask 类并在代码中使用它。

我认为这是一个相当丑陋的解决方案,例如:类型需要是一个类,因为结构需要进行并发检查,这意味着编译器在并发访问 和 时会semaphore出错result。据我所知,信号量应该是线程安全的,并且结果仅从一个上下文写入并由其余上下文读取。如果 T 是指针大小或更小,则写入是原子的,因此是“安全的”。在其他情况下,它可能不安全。尽管我可能会忽略一些并发边缘情况。开放征求建议。