Search code examples
swiftasync-awaitconcurrency

Async task spawned inside @MainActor is run on background?


I had the assumption that async tasks inherit the actor context so if my class is marked @MainActor the async tasks should run on the main thread? I think my assumption might be flawed.

In my code the first asyncTest() method is executed on the main thread but asyncMap() is executed on a background thread for some reason?

Here's the playground I've tested with:

import PlaygroundSupport
import Foundation

@MainActor
class AsyncTest {
    
    nonisolated init() {}
    
    func asyncTest() async {
        print(#function, "isMainThread:", Thread.isMainThread)
        
        let test: [String] = ["Hello", "World"]
        
        await test.asyncMap({ string in
            return string.uppercased()
        })
    }
}

extension Sequence {
    
    func asyncMap<T>(
        _ transform: (Element) async throws -> T
    ) async rethrows -> [T] {
        print(#function, "isMainThread:", Thread.isMainThread)
        
        var values = [T]()
        
        for element in self {
            try await values.append(transform(element))
        }
        
        return values
    }
}

let asyncTest = AsyncTest()
Task {
    await asyncTest.asyncTest()
}

PlaygroundPage.current.needsIndefiniteExecution = true

Which renders:

asyncTest() isMainThread: true
asyncMap(_:) isMainThread: false

Solution

  • No, async methods do not “inherit” the actor of their caller. The context used by asyncMap is dictated by the type in which it is defined (Sequence, which lacks a @MainActor qualifier) and of the function itself (which also does not have a MainActor qualifier). There is no reason that asyncMap would run on the main actor. It is not isolated to the main actor.


    So:

    • The asyncTest was defined inside a type that is isolated to the main actor, so asyncTest is also isolated to the main actor;

    • The asyncMap was defined inside a type (or extension) that is not isolated to any particular actor, so this async method is not isolated to the main actor;

    • The closure supplied to asyncMap was created inside asyncTest, which is isolated to the main actor, so this closure is also isolated to the main actor.


    For what it is worth, looking at your asyncMap idea, it is worth noting that Apple has already provided an asynchronous rendition of map, via AsyncSequence. To avail yourself of this, we can create an AsyncSequence from the array using async method in the Swift Async Algorithms package. Once you’ve added this package to your project, you can do things like:

    let test = ["Hello", "World"]
    let sequence = test.async.map { await foo(with: $0) }
    
    // and then
    
    for await element in sequence {
        print(element)
    }
    
    // or
    
    let result = await Array(sequence)
    

    Needless to say, I have replaced uppercased with a call to some asynchronous method (called foo in my example). If I were doing something synchronous, like uppercased, I would just use the standard map and be done with it:

    let result = test.map { $0.uppercased() }