Happy Friday! I thought I'd split out an idea from the review of SE-0461 into a separate mini pitch thread.
Consider the following code:
// This is really slow. You should never call it on an actor.
func someLongExpensiveOperation() { ... }
actor MyActor {
func waitForLongOperation() async {
someLongExpensiveOperation()
}
}
someLongExpensiveOperation
is expensive, and I don't want to run it on MyActor
, the main actor, or any other actor.
Today, if you want to move a bit of code off of an actor and onto the concurrent executor, you have three options:
- Wrap the code in a
nonisolated
andasync
function, and call that function.
func someLongExpensiveOperation() { ... }
nonisolated func someLongExpensiveOperationAsync() async {
someLongExpensiveOperation()
}
actor MyActor {
func waitForLongOperation() async {
await someLongExpensiveOperationAsync()
}
}
- Wrap the code in
await Task.detached { ... }.value
.
// This is really slow. You should never call it on an actor.
func someLongExpensiveOperation() { ... }
actor MyActor {
func waitForLongOperation() async {
await Task.detached {
someLongExpensiveOperation()
}.value
}
}
- Use a task executor preference.
// This is really slow. You should never call it on an actor.
func someLongExpensiveOperation() { ... }
actor MyActor {
func waitForLongOperation() async {
await withTaskExecutorPreference(globalConcurrentExecutor) {
someLongExpensiveOperation()
}
}
}
Option 1. is not very convenient, because you have to declare a function just to call it potentially only in one place. Option 2. breaks task structure in unnecessary ways. Option 3. does not work if the enclosing actor has a custom executor, such as the main actor. None of the options are terribly intuitive or ergonomic.
Instead, we could add an API to the concurrency library that accepts a closure, and runs the closure off of an actor. The API would look something like this (using the explicit, subject-to-change syntax from SE-0461):
@_alwaysEmitIntoClient
@execution(concurrent)
nonisolated public func runConcurrently<E, Result>(
_ fn: @escaping @execution(concurrent) () async throws(E) -> Result
) async throws(E) -> Result {
try await fn()
}
It could be called like this:
// This is really slow. You should never call it on an actor.
func someLongExpensiveOperation() { ... }
actor MyActor {
func waitForLongOperation() async {
// Offload this work from the actor.
await runConcurrently {
someLongExpensiveOperation()
}
}
}
The name of this API is terrible - please give me some better ideas
Thoughts?