Home > Software design >  Swfit: Trouble running async functions in background threads (concurrency)
Swfit: Trouble running async functions in background threads (concurrency)

Time:08-31

I'm having trouble making async functions run in background threads (to prevent blocking the main thread).

Below is a method that takes about 5 seconds to run. From what I've learned, it seemed like making the function async and marking it with await on function call would be enough. But it doesn't work as intended and still freezes up the UI.

EDIT Since it's stated that Swift 5.5 concurrency can replace DispatchQueue, I am trying to find a way to do this with only Async/Await.

EDIT_2 I did try removing the @MainActor wrapper, but it still seem to run on the main thread.

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
} }

What I've tried...

I've tried a few things, but the only way that made it run in the background was changing the function as below. But I know that using Task.detached should be avoided unless it's absolutely necessary, and I didn't think this is the correct use-case.

    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
        }

    }

CodePudding user response:

Writing async on a function doesn’t make it leave the thread. You need a continuation and you need to actually leave the thread somehow.

Some ways you can leave the thread using DispatchQueue.global(qos: .background).async { or use Task.detached.

But the most important part is returning to the main thread or even more specific to the Actor's thread.

DispatchQueue.main.async is the "old" way of returning to the main thread it shouldn't be used with async await. Apple as provided CheckedContinuation and UncheckedContinuation for this purpose.

Meet async/await can elaborate some more.

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)
            }
        }
    }
    
}
//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
    }
}

CodePudding user response:

You have answered your own question - You can use structured concurrency to solve your problem.

Your problem occurs because you have use the @MainActor decorator on your class. This means that it executes on the main queue.

You can either remove this decorator or, as you have found, use structured concurrency to explicitly create a detached task and the use a main queue task to provide your result.

Which approach you use depends on what else this class does. If it does a lot of other work that needs to be on the main queue then @MainActor is probably a good approach. If not then remove it.

CodePudding user response:

try something like this approach.

Note that just adding async to func generateNumbers() does not make it async.

struct ContentView: View {
    @StateObject private var numberManager = NumberManager()
    
    var body: some View{
        TabView{
            VStack{
                // DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
                Text("First number ") // <-- for testing
                    .task {
                        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
    }
}

@MainActor class NumberManager: ObservableObject {
    @Published var numbers: [Double]?
    
    func generateNumbers() async  {
        DispatchQueue.global(qos: .background).async { // <-- here background work
            var numbers = [Double]()
            numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
            DispatchQueue.main.async {  // <-- here update the UI on main
                self.numbers = numbers
            }
        }
    }
    
}
  • Related