You say elsewhere that Task {…}
“starts running on its own thread asynchronously”. It doesn’t. It creates a new task for the current actor.
As the docs for Task {…}
says, it:
Runs the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.
But an actor can only be running one thing at a time. So, that means that until the current actor finishes execution (or hits an await
“suspension point”), this Task
will simply not have an opportunity to run.
If you want it to start something on a separate thread, consider Task.detached {…}
. The docs for detached
tell us that it:
Runs the given throwing operation asynchronously as part of a new top-level task.
So, a detached task may avail itself of a thread pulled from the cooperative thread pool, running in parallel with the current code. (Obviously, if the cooperative thread pool is busy running other stuff, even Task.detached
might not start immediately: It depends upon your hardware and how busy the the cooperative thread pool might be with other tasks.)
But the following will likely show “in task” before “done counting”. (Technically, there is a “race” and either message could appear first, but presumably your for
loop is slow enough that you will consistently see “in task” message first.)
func test() {
Task.detached {
print("in task")
}
for _ in 0 ..< 10_000_000 { }
print("done counting")
}
Needless to say, you never would spin like this unnecessarily. I presume that the spinning for
loop is here merely to manifest the problem at hand. But in more practical examples, where you have an await
suspension point, the problem simply disappears. Consider the following:
func downloads(_ urls: [URL]) async {
Task {
print("in task")
}
for url in urls {
await download(url)
}
print("done counting")
}
Here, the detached task is not needed because we have an await
in this routine, so the is ample opportunity for the Task
to be run. This obviously is a contrived example (i.e., I generally would avoid unnecessary unstructured concurrency), but hopefully it illustrates that if your loop doesn’t actually block, but rather has await
suspension points, using the current actor is not a problem.
Note, the detached task example earlier is a bit backwards from the common pattern. (I did that to preserve your code order.) Usually, it is the blocking code that would go inside the detached task. E.g., consider an example where we want to call some image processing service that is synchronous, computationally intensive, and blocks the caller; this is a more practical example of a detached task, explicitly avoiding the blocking of the current actor:
func process(_ image: UIImage) async throws {
print("starting")
processedImage = try await Task.detached {
ImageService.shared.convertToBlackAndWhite(image)
}.value
print("done")
}