Home > database >  The callback inside Task is automatically called on the main thread
The callback inside Task is automatically called on the main thread

Time:01-17

Once upon the time, before Async/Await came, we use to make a simple request to the server with URLSession dataTask. The callback being not automatically called on the main thread and we had to dispatch manually to the main thread in order to perform some UI work. Example:

DispatchQueue.main.async {
    // UI work
}

Omitting this will lead to the app to crash since we try to update the UI on different queue than the main one.

Now with Async/Await things got easier. We still have to dispatch to the main queue using MainActor.

await MainActor.run {
 // UI work
}

The weird thing is that even when I don't use the MainActor the code inside my Task seems to run on the main thread and updating the UI seems to be safe.

Task {
       let api = API(apiConfig: apiConfig)
        
       do {
              let posts = try await api.getPosts() // Checked this and the code of getPosts is running on another thread.
              self.posts = posts 
              self.tableView.reloadData()
              print(Thread.current.description)
          } catch {
              // Handle error
          }
}

I was expecting my code to lead to crash since I am trying to update the table view theorically not from the main thread but the log says I am on the main thread. The print logs the following:

<_NSMainThread: 0x600003bb02c0>{number = 1, name = main}

Does this mean there is no need to check which queue we are in before performing UI stuff?

CodePudding user response:

Regarding Task {…}, that will “create an unstructured task that runs on the current actor” (see Swift Concurrency: Unstructured Concurrency). That is a great way to launch an asynchronous task from a synchronous context. And, if called from the main actor, this Task will also be on the main actor.

In your case, I would move the model update and UI refresh to a function that is marked as running on the main actor:

@MainActor
func update(with posts: [Post]) async {
    self.posts = posts 
    tableView.reloadData()
}

Then you can do:

Task {
    let api = API(apiConfig: apiConfig)
            
    do {
        let posts = try await api.getPosts() // Checked this and the code of getPosts is running on another thread.
        self.update(with: posts)
    } catch {
        // Handle error
    }
}

And the beauty of it is that if you’re not already on the main actor, the compiler will tell you that you have to await the update method. The compiler will tell you whether you need to await or not.

If you haven’t seen it, I might suggest watching WWDC 2021 video Swift concurrency: Update a sample app. It offers lots of practical tips about converting code to Swift concurrency, but specifically at 24:16 they walk through the evolution from DispatchQueue.main.async {…} to Swift concurrency (e.g., initially suggesting the intuitive MainActor.run {…} step, but over the next few minutes, show why even that is unnecessary, but also discuss the rare scenario where you might want to use this function).


As an aside, in Swift concurrency, looking at Thread.current is not reliable. Because of this, this practice is likely going to be prohibited in a future compiler release.

If you watch WWDC 2021 Swift concurrency: Behind the scenes, you will get a glimpse of the sorts of mechanisms underpinning Swift concurrency and you will better understand why looking at Thread.current might lead to all sorts of incorrect conclusions.

  • Related