Swift Concurrency Deep Dive [5] — Actor

Enebin
5 min readNov 6, 2022

--

Photo by Bozhin Karaivanov on Unsplash

This post is written for a deeper understanding of Swift concurrency, that is, async/await.

I’ve collected information from reliable sources like Apple Developer’s Document, Swift-evolution repository or Swift Language Guide as much as possible, but may contain incorrect information. In that case, please let me know in the comments.

I highly recommend you to read my previous series

Actor model

Before we get into the Swift’s actor, we should first know about what the actor model is.

Actor model is introduced to help manage the shared resources in concurrent programing. There’re two features the actor has.

  1. A task accessing an actor must be executed one by one, like a serial queue.
  2. Mutable state must not be shared among other actors.

Due to these features, actor can be free of deadlock and race conditions, which have been deep-seated problems of concurrent programming.

Actor in Swift

💡 from Language Guide

You can use tasks to break up your program into isolated, concurrent pieces. Tasks are isolated from each other, which is what makes it safe for them to run at the same time, but sometimes you need to share some information between tasks. Actors let you safely share information between concurrent code.

Swift concurrency also adopted the actor model. You can create your own actors using the actor keyword like creating a class(suggestion document).

Actor can be used when you want to keep your properties isolated from other tasks described as “Sea of concurrency”, referring to the WWDC session.

Relationship with Task

From Visualize and optimize Swift concurrency — WWDC22

Remember when we create a task, it inherits current actor’s context? In here, what is the relationship between actors and tasks?

Task, as a unit of job in Swift concurrency, contains and does the allocated instructions. Actor, on the other hands, is quite different from task.

Actor is a type of Swift. It contains variables which is protected from being accessed from multiple places at the same time. Also, it can have functions that can be performed with those variables.

In here, we should remind that the task is just a unit of job for Swift concurrency, not a unit of actor. It means that a task, basically, has nothing to do with the actor. There can be a task running inside the actor and also a task that doesn’t.

It can be confusing. One thing you should know in here is that all tasks are not necessarily running on the actor. Task can exist everywhere in Swift concurrency’s context. In terms of task, actor can be regarded as assistive type for data sharing between tasks.

Relationship with unstructured concurrency

Unstructured concurrency created by Task.init inherits the actor it belongs to, according to the document. On the other hand, if it is created detached by Task.detached, it doesn’t inherit its current actor.

Now you might be wondering that if there’s no actor the task belongs to, what’s the difference between a detached task and a normal task?

By the answer from the developer of Swift, they’re same from thread’s perspective because both don’t inherit any actors and will be running in cooperative thread pool.

However, generally, it’s not equivalent. “Creating unstructured Tasks via Task.init does not only inherit actor context, it also inherits task local values. Task.detached does not”, according to the answer.

Cooperative thread pool

from Swift concurrency: Behind the scenes — WWDC21

💡 from the Swift forum

The Task(not on any actor), being unstructured, runs on the co-operative thread pool, just like any other Swift Task. It does not run on any actor, but what @Douglas_Gregor calls the “sea of concurrency”.

If tasks doesn’t belong to any actors, where are they running then? Task that runs independently of actor uses a shared thread called the “cooperative thread pool”.

The cooperative thread pool is a kind of pre-made thread candidate group for executing tasks in Swift concurrency. In addition, since cooperative thread pool has more than one thread, it’s possible to run tasks parallelly in the pool.

Along with an unstructured concurrency created with Task, a child task created with async let or TaskGroup must be executed in cooperative thread pool if it doesn’t belong to any actors.

Performance optimization for actor and thread pool

from Visualize and optimize Swift concurrency — WWDC22

Since an actor only executes one task at a time, running all tasks on an actor can cause a bottleneck. Therefore, it is appropriate to distribute allocated jobs to the other threads or actors.

For that, we can use the nonisolated keyword. It makes the actor’s tasks to access the actor only when they are needed. By doing this, the rest part of each task can do their non-actor-related jobs on the cooperative thread pool so resolving the danger of bottleneck.

MainActor

from Protect mutable state with Swift actors — WWDC21

There’s a special actor called MainActor. The MainActor does exactly same thing the main queue of DispatchQueue does. MainActor is the only actor in the system that uses the main thread.

Like the main queue we’ve used, the tasks that need to be processed with the highest priority, such as UI jobs, should be executed in here. If you see the latest version of UIKit, you can catch that UI-related classes, such as UIViewController have already changed to MainActor.

You can create it using the @MainActor wrapper.

--

--