When does a Task get scheduled to the Cooperative Thread Pool?

If I create a Task from a viewDidLoad or a viewDidAppear function, I notice that the work inside the Task only starts after returning from the calling function. I didn't expect this because I assumed creating a Task using its initialiser was similar to doing DispatchQueue.global().async { } (asynchronously gets scheduled onto the Cooperative Thread Pool).

Can anyone explain why a non-throwing Task created using its initialiser doesn't get called instantly, but rather, has to wait for the function that created it to return? Specifically, when the task isn't running on behalf of any actor (such as when created from a function on the main thread).

P.S. I am assuming two things:

  1. Tasks created on the main thread are not run on any actor, rather, are scheduled on the cooperative thread pool
  2. Creating a Task using its initialiser is similar to DispatchQueue.global().async { }
1 Like

My understanding: UIView is tagged @MainActor so by default Task objects created from your view methods inherit this actor and will run on the main actor, ie. on the main thread. So this is more or less equivalent to DispatchQueue.main.async whose closure obviously cannot run before viewDidLoad is done, since it is also called from the main thread.

In your case with Swift concurrency if you want your Task to start earlier, you’d need to use Task.detached.

Ah I see. However the difference is that it isn’t a “Queue” as such because the Main Actors executor can execute Tasks in any order it deems the most beneficial (if I had put multiple tasks in the View did load function). Am I right?

But wait I just realised something. If they all get put on the Main Actors executor, and the main actors executors work is always done on the main thread, won’t that defeat the purpose of tasks? If I had put networking data in that task, it would not happen on a background thread but rather the main thread, right?

No. When you call Task { }, the only work guaranteed to be on the main actor is the synchronous work in the closure. Anything you await will be called wherever the callee has specified.

func someSyncWork() {}
func someAsyncWork() async {}
@MainActor
func someMainWork() async {}

...

func viewDidLoad() {
  super.viewDidLoad()

  Task {
    someSyncWork() // Executed on main actor due to Task capturing actor context by default.
    await someAsyncWork() // Executed on the global executor because someAsyncWork doesn't specify an actor context.
    await someMainWork() // Executed on main actor because it specifies main actor.
  }  
}

Task also executes its content concurrently, so even for a serial executor like the main actor, separately enqueued tasks may execute in any order. Two tasks enqueued in the same method are allowed to execute in arbitrary order, even if they will nearly always execute sequentially in practice.

6 Likes