Trouble running async functions in background threads (concurrency)

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

Leave a Comment