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