What is an Actor and What Are They Used For?

AsyncLearn
3 min readNov 20, 2023

--

When working with asynchronous code, we often turn to concurrency to take advantage of multiple threads’ processing capabilities. This can lead to race conditions, which occur when two or more threads attempt to access/modify values simultaneously. We can avoid these problems by creating objects of type actor.

An actor is a type that provides synchronization for shared state that is not constant, meaning it can be modified at runtime.

Why is it important to avoid race conditions? Avoiding race conditions is one of the primary purposes of actors. The characteristics of race conditions include:

  • Non-deterministic behavior, functions don’t always produce the same result.
  • Caused by mutable states shared by multiple objects.
  • Two or more threads attempt to access the same data, with at least one trying to modify that data.

Characteristics of an Actor

Next, let’s look at the characteristics of an actor using the following example:

actor VisitsCounter {
var numberOfVisits: Int

func addNewVisit() {
numberOfVisits += 1
}
}

In terms of syntax, an actor doesn’t differ much from a structure or a class. In this case, if it weren’t for the actor keyword, they would be identical.

To call the addNewVisit() method from outside the actor in our code or to access numberOfVisits, we must use await. For example:

let counter = VisitsCounter()

Task {
await counter.addNewVisit()
}

This allows actors to coordinate access to state and prevent race conditions since, by using await, we add our request to the actor's queue, and the actor decides when to respond to the request. Calls are suspended until resumed by the actor to provide a response.

On the other hand, calls within an actor are always synchronous, so we don’t have to use await. That's why in our actor, we change the value of numberOfVisits without using await.

Other Features of an Actor to Keep in Mind

  • Actors have their own state, which is isolated from the rest of the program.
  • All access to the state is performed through the actor. This ensures that access to the state is mutually exclusive, meaning that two threads can never access it at the same time.
  • They are reference types, similar to classes.
  • They do not support inheritance.
  • An actor’s state can change during suspension, i.e., while waiting for code preceded by await to execute. It is recommended to verify that the state is as expected after executing code using await.

Swift allows us to use an actor to ensure that code runs on the main thread, the Main Actor.

Using nonisolated to Allow Synchronous Execution

We can use the nonisolated modifier to indicate that parts of an actor's code should be accessed as if they were outside the actor. In other words, we should use nonisolated when it's necessary for a method to run synchronously but is implemented within the actor.

It’s important to know that methods marked with nonisolated cannot access mutable variables within the actor.

Imagine we need to make our actor conform to the Hashable protocol. We would have to use the following code:

extension VisitsCounter: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(numberOfVisits)
}
}

Hashable needs to be executed synchronously, so if we don't use nonisolated, the compiler will show the following error:

actor-isolated method hash(into:) cannot satisfy synchronous requirement

Sendable

If we need to create properties of class type within an actor, we must ensure that these properties conform to Sendable.

Sendable refers to types whose properties can be safely shared in concurrent code. To achieve this, all properties that make up the class (or structure) must conform to Sendable.

Sendable Functions and Closures

For functions and closures, we use the @Sendable attribute, but there are some restrictions to consider:

  • It cannot capture a mutable local variable because this could lead to race conditions.
  • Anything captured by the closure must conform to Sendable.
  • A synchronous closure can never be isolated, as this would allow us to execute code inside the actor from outside.

If you want to read the Spanish version of this article, you can find it here: https://asynclearn.com/blog/que-es-un-actor/

--

--

AsyncLearn

Stay up-to-date in the world of mobile applications with our specialised blog.