In the Apple Docs, it says
Tasks can start running immediately after creation; you don’t explicitly start or schedule them.
However, in my code, the Task only starts running when the function that calls it goes out of scope.
func test() {
Task {
print("in task")
}
for _ in 0 ..< 10_000_000 { }
print("done counting")
}
This code when executed hangs / waits for a certain amount of time and then always prints:
done counting
in task
I expected it to print
in task
done counting
Can someone explain what I’m missing from what’s written in the Apple Docs?
3
Answers
You’re probably testing in the simulator, which serializes Swift Concurrency to a single thread. Note the phrase "can start running." It does not promise that it will.
You also are blocking the whole thread with your loop. You may want to add a
Task.yield()
in the block to allow other things to run. Structured concurrency is all about cooperating tasks that await when they would otherwise block. It’s not specifically about parallelism of CPU-intensive tasks. Tasks are not intended to block their thread (doing so violates their contract to "make progress").Tasks also default to the same context that they are started in. If you want a new independent task, you want to use
Task.detached {}
.In Swift’s concurrency model, tasks created using the Task API are not executed immediately upon creation. Instead, they are added to the system’s task scheduler and executed asynchronously at some point in the future.
In the code snippet you provided, the Task block is created but not executed immediately. The for loop following the Task block is executed before the Task block is scheduled for execution.
If you want to wait for the task to complete before executing the rest of the code, you can use the await keyword to pause the current task and wait for the child task to complete. For example:
In this version, the await keyword is used to pause the current task and wait for the child task to complete before the print("done counting") statement is executed. This ensures that the message from the Task is printed before the "done counting" message.
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: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”), thisTask
will simply not have an opportunity to run.If you want it to start something on a separate thread, consider
Task.detached {…}
. The docs fordetached
tell us that it: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.)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 anawait
suspension point, the problem simply disappears. Consider the following:Here, the detached task is not needed because we have an
await
in this routine, so the is ample opportunity for theTask
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 hasawait
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: