在并发执行的代码中使用对捕获变量的引用

Arj*_*jan 41 async-await swift5 swiftui

更新2:我怀疑这个问题因为我描述的可能的解决方案而被投票。为了清楚起见,突出显示了它。

更新1:这个问题得到了很多人的关注。如果您认为可以通过您自己遇到错误的情况来增强问题,请在评论中简要描述您的情况,以便我们可以使此问答更有价值。如果您对您的问题版本有解决方案,请将其添加为答案。


Task.detached我想在使用和函数执行异步后台工作后更新 UI async

但是,我在构建过程中收到构建错误错误Reference to captured var 'a' in concurrently-executing code

我尝试了一些方法,在更新 UI 之前将变量转换为 alet constant是唯一有效的方法。为什么我需要先创建一个 let 常量才能更新 UI?还有其他选择吗?

class ViewModel: ObservableObject {
    
    @Published var something: String?
    
    init() {
        Task.detached(priority: .userInitiated) {
            await self.doVariousStuff()
        }
    }
    
    private func doVariousStuff() async {
        var a = "a"
        let b = await doSomeAsyncStuff()
        a.append(b)
        
        something = a /* Not working,
        Gives
           - runtime warning `Publishing changes from 
           background threads is not allowed; make sure to 
           publish values from the main thread (via operators 
           like receive(on:)) on model updates.`
         or, if `something` is @MainActor:
           - buildtime error `Property 'something' isolated 
           to global actor 'MainActor' can not be mutated 
           from this context`
         */



        await MainActor.run { 
            something = a 
        } /* Not working, 
        Gives buildtime error "Reference to captured var 'a' 
        in concurrently-executing code" error during build
         */


        DispatchQueue.main.async { 
            self.something = a 
        } /* Not working,
        Gives buildtime error "Reference to captured var 'a' 
        in concurrently-executing code" error during build
         */


        /*
         This however, works!
         */
        let c = a
        await MainActor.run {
            something = c
        }

    }
    
    private func doSomeAsyncStuff() async -> String {
        return "b"
    }
} 
Run Code Online (Sandbox Code Playgroud)

dav*_*sdk 27

简而言之,something必须从主线程进行修改,并且只有 Sendable 类型可以从一个 actor 传递到另一个 actor。让我们深入研究细节。

something必须从主线程修改。这是因为必须从主线程修改@Publishedan 中的属性。ObservableObject缺少这方面的文档(如果有人找到官方文档的链接,我将更新此答案)。但由于 an 的订阅者ObservableObject可能是 SwiftUI View,所以这是有道理的。Apple 可以决定View在主线程上订阅和接收事件,但这会隐藏这样一个事实:从多个线程发送 UI 更新事件是危险的。

只有可发送类型可以从一个参与者传递到另一个参与者。有两种方法可以解决这个问题。首先我们可以使aSendable。其次,我们可以确保不跨越aActor 边界,并使所有代码在同一个 Actor 上运行(在这种情况下,它必须是 Main Actor,因为它保证在主线程上运行)。

让我们看看如何使其a可发送并研究以下案例:

await MainActor.run { 
    something = a 
}
Run Code Online (Sandbox Code Playgroud)

函数中的代码doVariousStuff()可以从任何参与者运行;我们称其为 Actor A。a属于 Actor A,并且必须将其发送给 Main Actor。由于a不符合 Sendable,编译器看不到任何在 Main Actor 上读取a时不会更改的保证。a这在 Swift 并发模型中是不允许的。为了给编译器这样的保证,a必须是可发送的。一种方法是使其保持不变。这就是为什么这有效:

let c = a
await MainActor.run {
    something = c
}
Run Code Online (Sandbox Code Playgroud)

即使可以改进为:

await MainActor.run { [a] in
    something = a 
}
Run Code Online (Sandbox Code Playgroud)

捕获a为常数。还有其他 Sendable 类型,详细信息可以在这里找到https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID649

解决这个问题的另一种方法是让所有代码在同一个参与者上运行。最简单的方法是按照 Asperi 的建议ViewModel进行标记。@MainActor这将保证doVariousStuff()从 Main Actor 运行,因此它可以设置something. 作为旁注,athen 属于主要演员,所以(即使它毫无意义)await MainActor.run { something = a }可以工作。

请注意,参与者不是线程。Actor A 可以从任何线程运行。它可以在一个线程上启动,然后在任何一个线程之后继续在另一个线程上await。它甚至可以部分在主线程上运行。重要的是,一个参与者一次只能从一个线程运行。任何 Actor 都可以从任何线程运行这一规则的唯一例外是仅在主线程上运行的 Main Actor。


Asp*_*eri 12

让你的可观察对象成为主角,比如

@MainActor                                // << here !!
class ViewModel: ObservableObject {

    @Published var something: String?

    init() {
        Task.detached(priority: .userInitiated) {
            await self.doVariousStuff()
        }
    }

    private func doVariousStuff() async {
        var a = "a"
        let b = await doSomeAsyncStuff()
        a.append(b)

        something = a         // << now this works !!
    }

    private func doSomeAsyncStuff() async -> String {
        return "b"
    }
}
Run Code Online (Sandbox Code Playgroud)

使用 Xcode 13 / iOS 15 进行测试

  • 这不是让一切都在主线程上运行吗? (4认同)