What is an Actor and What Are They Used For?
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 usingawait
.
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/