在后台线程(并发)中运行异步函数出现问题

3

我在将异步函数运行在后台线程方面遇到了麻烦(以防止阻塞主线程)。

下面是一个需要大约5秒钟来运行的方法。 从我所学到的知识来看,似乎只需使函数async并在函数调用上标记await就足够了。但它并没有按预期工作,仍然会冻结UI。

编辑 由于Swift 5.5并发可以替换DispatchQueue,我正在尝试找到一种仅使用Async / Await实现此目的的方法。

编辑_2 我确实尝试过删除@MainActor包装器,但它仍然似乎在主线程上运行。

NumberManager.swift

@MainActor class NumberManager: ObservableObject {
@Published var numbers: [Double]?    

func generateNumbers() async  {

        var numbers = [Double]()
        numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
        self.numbers = numbers
    // takes about 5 seconds to run...
} }

ContentView

struct ContentView: View {
@StateObject private var numberManager = NumberManager()

var body: some View{
    TabView{
        VStack{
            DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
                .onAppear() {
                    Task {
                        // Runs in the main thread, freezing up the UI until it completes. 
                        await numberManager.generateNumbers()
                    }
                }

        }
        .tabItem {
            Label("One", systemImage: "list.dash")
        }
        Text("Hello")
            .tabItem {
                Label("Two", systemImage: "square.and.pencil")
            }
    }

}

var isNumbersValid: Bool{
    numberManager.numbers != nil && numberManager.numbers?.count != 0
} }

我尝试过的方法...

我尝试了几种方法,但唯一使它在后台运行的方式是更改函数如下。但我知道除非绝对必要,否则应避免使用Task.detached,并且我认为这不是正确的用例。

    func generateNumbers() async  {
    Task.detached {
        var numbers = [Double]()
        numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
        await MainActor.run { [numbers] in
            self.numbers = numbers
        }

    }

你已经使用@MainActor标记了你的类 - 所以它仍然在主队列上执行。移除它或者,正如你发现的那样,使用结构化并发将任务从主队列中分离出来。 - Paulw11
请看这个 https://dev59.com/0sPra4cB1Zd3GeqPcUW7#70645342 - Timmy
仅仅去掉MainActor是否可以解决问题?我尝试了一下,但它仍在主线程中运行。@Paulw11 - Lucas Chae
在函数上编写异步并不意味着它会离开线程。您需要一个continuation,并且需要以某种方式实际离开线程。您的代码中没有任何内容离开线程并告诉并发何时返回。 - lorem ipsum
3个回答

5

在函数上写async并不能使其离开线程,你需要一个继续的方法并且需要以某种方式实际离开线程。

一些使用DispatchQueue.global(qos: .background).async {或者使用Task.detached可以离开线程的方式。

但最重要的部分是返回到主线程,甚至更具体地说,返回到Actor线程。

DispatchQueue.main.async是回到主线程的“旧”方式,它不应该与async await一起使用。苹果为此提供了CheckedContinuationUncheckedContinuation

遇见async/await可以进一步阐述。

import SwiftUI

struct ConcurrentSampleView: View {
    //Solution
    @StateObject var vm: AsyncNumberManager = .init()
    //Just to create a project that can show both scenarios.
    //@StateObject var vm: NumberManager = .init()

    @State var isLoading: Bool = false
    var body: some View {
        HStack{
            //Just to visualize the thread being released
            //If you use NumberManager the ProgressView won't appear
            //If you use AsyncNumberManager the ProgressView WILL appear
            if isLoading{
                ProgressView()
            }
            
            Text(vm.numbers == nil ? "nil" : "\(vm.numbers?.count.description ?? "")")
        }
        //.task is better for iOS 15+
        .onAppear() {
            Task{
                isLoading = true
                await vm.generateNumbers()
                isLoading = false
            }
        }
        
    }
}

struct ConcurrentSampleView_Previews: PreviewProvider {
    static var previews: some View {
        ConcurrentSampleView()
    }
}

@MainActor
class AsyncNumberManager: ObservableObject {
    @Published var numbers: [Double]?
    
    func generateNumbers() async  {
        numbers = await concurrentGenerateNumbers()
    }
    
    private func concurrentGenerateNumbers() async -> [Double]  {
        typealias Cont = CheckedContinuation<[Double], Never>
        return await withCheckedContinuation { (cont: Cont) in
            // This is the asynchronous part, have the operation leave the current actor's thread.
            //Change the priority as needed
            //https://developer.apple.com/documentation/swift/taskpriority
            Task.detached(priority: .utility){
                var numbers = [Double]()
                numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
                //This tells the function to return to the actor's thread
                cont.resume(returning: numbers)
            }
        }
    }
    //Or something like this it just depends on the true scenario
    private func concurrentGenerateNumbers2() async -> [Double]  {
        // This is the asynchronous part, have the operation leave the actor's thread
        //Change the priority as needed
            //https://developer.apple.com/documentation/swift/taskpriority
        return await Task.detached(priority: .utility){
            var numbers = [Double]()
            numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
            return numbers
        }.value
        
    }
    
}
//Incorrect way of applying async/await. This doesn't actually leave the thread or mark when to return. Left here to highlight both scenarios in a reproducible example.
@MainActor
class NumberManager: ObservableObject {
    @Published var numbers: [Double]?
    
    func generateNumbers() async  {
        
        var numbers = [Double]()
        numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
        self.numbers = numbers
    }
}

0

你已经回答了自己的问题 - 你可以使用结构化并发来解决你的问题。

你的问题出现在你在类上使用了@MainActor修饰符。这意味着它在主队列上执行。

你可以删除这个修饰符,或者像你发现的那样,使用结构化并发来显式地创建一个分离的任务,并使用主队列任务来提供你的结果。

你使用哪种方法取决于这个类还做了什么其他工作。如果它做了很多需要在主队列上完成的其他工作,那么@MainActor可能是一个不错的选择。如果不是,那就把它删除。


谢谢,所以我认为像我这样使用结构化并发的方式是这些场景中的实际用例。顺便说一句,我尝试了一个没有MainActor的新构建版本,但它仍然在主线程上运行。有什么想法吗? - Lucas Chae
请勿删除 MainActor 装饰器。对观察属性的任何更改都必须在主线程上完成。 - Cheezzhead

-1

@MainActor 属性包装器不是(仅仅)让你的可观察对象在主线程上运行的原因。 @StateObject 才是实现这一点的关键。因为对对象的更改将更新 UI,所以这是合理的。

删除 @MainActor 包装器并不是解决方案,因为任何对 @Published 属性的更改都必须在主线程上完成(因为它们会更新 UI)。你也不想运行 Task.detached,至少如果该任务要更改任何 @Published 属性,则出于同样的原因。

通过将你的 generateNumbers 函数标记为 async,从主线程调用它时可以使用 await - 这允许任务暂停而不会阻塞主线程。这就是并发产生的原因。

extension NumberManager {
    func loadNumbers() {
        // This task will run on the main thread
        Task {
            // `await` tells the Swift executor that this method can run in the background, 
            // and the main thread can continue doing other things while it waits for its result
            self.numbers = await self.generateNumbers()
        }
    }

    func generateNumbers() async -> [Double] {
        return (1...10_000_000).map { _ in Double.random(in: -10...10) 
    }
}

struct ContentView: View {
    @StateObject private var numberManager = NumberManager()
    
    var body: some View {
        TabView{
            VStack{
                DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
            }
        }

        .onAppear { numberManager.loadNumbers() }
    }
}

一个更完整的loadNumbers方法还可以存储对任务的引用,使您能够取消或重新启动正在运行的任务。但是现在我们有优秀的.task(priority:_:)来为您完成所有这些工作。它会自动管理任务的生命周期,这意味着更少的样板代码。
struct ContentView: View {
    @StateObject private var numberManager = NumberManager()
    
    var body: some View {
        TabView{
            VStack{
                DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
            }
        }

        // generate numbers
        .task(.priority: .high) {
            numberManager.numbers = await numberManager.generateNumbers
        }
    }
}

据我所知,这是在Swift 5.7中在后台线程上执行昂贵计算的最简洁方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接