Logo for tanaschita.com

Understanding actors in Swift

Learn how to use Swift actors to prevent data racesç when working with concurrency.

12 Sep 2021 · 5 min read

At the WWDC 2021, Apple introduced Swift actors as a new synchronization tool to prevent data races when working with concurrent code. This feature is part of other concurrency developments like Swift's async/await syntax.

How do Swift actors prevent data races?

Data races occur when shared mutable data is accessed from different threads while one or more of the threads writes the data.

Actors prevent data races by isolating data from the outside world mainly doing 3 things:

  • they make sure that only one task (thread) at a time accesses their data
  • they deny synchronious access to their mutable state from the outside
  • they deny direct modification on their mutable state.

The actor type

Actors are represented by a new reference type called actor.

actor Shop {
let id = "abc"
var itemsCount = 10
func purchase() {
itemsCount-=1
}
}

As we can see in the example above, defining an actor works the same way as with classes or structs. The main difference lies in the outside usage.

Using async/await to access data from an actor

The only actor data we get synchronious access to from the outside is immutable data i.e. constants.

let shop = Shop()
print(shop.id)

If we want to access any other properties or methods of an actor from the outside, we need to mark potential suspension points with await. Otherwise the call would fail with a compiler error.

let shop = Shop()
Task {
await shop.purchase()
print(await shop.itemsCount)
}

Notice, that we didn't define the purchase method as async when writing our actor, but we still need to await it. This is the essence of an actor. It only gives us asynchronious access to it's mutable data. If some other task has already called purchase, our purchase() call would suspend and wait for the other one to finish.

Actor isolation

So when accessing the data of an actor, the following rules apply:

  • reading & updating properties or calling methods from the inside can happen synchronously
  • reading immutable properties (constants) from the outside can happen synchronously
  • reading properties or calling methods from the outside has to happen asynchronously by using the await keyword
  • updating properties from the outside will result in a compiler error

These guarantees are also known as actor isolation.

Actor non-isolated declarations

There may be cases, were we want to provide synchronious access to a method or computed property of an actor. To achieve this, we can use the nonisolated declaration.

nonisolated func log(message: String) {
print("\(id) \(message)")
}

The log method can now be called from the outside without an await. Inside the nonisolated method, we are only allowed to use nonisolated properties (constants) and methods.

Sendable types

So far, we have only used value types like String or Int when working with actors. In a real world application, we most likely will use our custom structures inside them.

actor Shop {
var owner: Owner
}

When using our own defined types, we need to be careful. If the Owner type is a class, we are able to get a reference to the mutable state of the actor and might end up with a data race after all.

var owner = await shop.owner
owner.name = "abc"

This is why the types we pass into or get from the actor should be thread safe. Mutable classes are not thread safe, but structs and actors are.

To check the thread safety of a type we can use the Sendable protocol which indicates that the given type can be safely used in concurrent code.

class Owner: Sendable { // compiler error: Non-final class 'Owner' cannot conform to Sendable
var name: String
}

Since mutable classes are not thread safe we get a compiler error. Changing our type to a struct or an actor resolves the error.

struct Owner: Sendable {
var name: String
}

Conclusion

Bugs produced by data races are hard to debug since the outcome may be different every time depending on when and how the data race occurs. The new actor type in Swift provides us with a powerful and easy way to synchronize our concurrent code.

Related tags

Written by

Articles with related topics

Latest articles and tips