How to migrate your code to Swift’s Concurrency - Part III: Why to use Actors?

Co-author: Ignacio Cerviño LinkedIn

You should consider using actors in the following scenarios:

  • Protecting Shared Mutable State: When you have data that might be accessed or modified from multiple concurrent tasks or threads.

  • Simplifying Concurrency Management: To avoid manual synchronization mechanisms like locks, semaphores, or queues.

  • Encapsulating State with Controlled Access: When you want to ensure that the internal state cannot be altered unexpectedly from outside the actor.

Sample

In this sample:

  • Counter is an actor that manages a simple integer value.

  • The value property is private and can only be accessed or modified within the actor.

  • The methods increment() and getValue() provide controlled access to the actor's state.

Using Counter Actor

In this usage:

  • We create an instance of Counter.

  • We increment the counter twice by calling await counter.increment(), ensuring safe access.

  • After all tasks complete, we retrieve the final value.

Considerations

  • Asynchronous Access: Accessing an actor's methods from outside its context requires the use of await because these methods are asynchronous by default.

  • No Direct Property Access: You cannot directly access an actor's properties from outside. You must use methods provided by the actor.

  • Isolation Guarantee: Actors guarantee that their mutable state is protected from data races, making your concurrent code safer.

What Are Actors in Simple Terms?

In Swift, actors are used to handle concurrency by ensuring that shared mutable state is isolated and protected. Unlike regular objects, which can be accessed from any thread, actors ensure that their properties and methods are accessed safely, even in a concurrent environment.

You can think of an actor as a guarded object that only allows one task to access its state at a time. This prevents race conditions, where two parts of a program might try to change the same piece of data simultaneously. In Swift, actors help us manage concurrency more safely and with fewer bugs.

History of Actors

The Actor model was originally introduced in 1973 by Carl Hewitt in a paper aimed at solving concurrency issues in artificial intelligence systems. The model treats "actors" as the basic unit of computation, capable of receiving messages, processing them, and sending messages to other actors. It was designed to be modular, scalable, and highly concurrent, making it useful in distributed systems where many parts of a program must run simultaneously without interfering with each other.

The original goal of the actor model was to simplify and modularize concurrent systems, a goal it has achieved in languages and frameworks that rely heavily on safe parallel execution.

How Actors Relate to Swift Concurrency

With the introduction of Swift’s concurrency model in Swift 5.5, actors provide a powerful tool to replace older approaches like completion handlers and GCD. Swift's concurrency model uses async/await to handle asynchronous tasks in a clearer, more structured way. Actors are a key part of this model, ensuring that mutable state is accessed safely when working with multiple threads.

If you’ve written code using completion handlers, you probably managed asynchronous tasks by passing callbacks. In Swift’s new model, actors handle much of the heavy lifting for you. They allow for isolated state, meaning that the data inside an actor is only accessible by one task at a time, avoiding data races without having to write manual locking code. This makes your code cleaner and easier to reason about.

Actors Model applied with GCD and Completion Handlers

In this example, we use GCD to manage concurrent access to the balance property, ensuring thread safety by using a serial queue to process all deposit and withdraw requests one at a time. We'll use completion handlers to asynchronously return results.

Explanation of the code above

  • Serial Queue: We use a DispatchQueue to ensure thread-safe access to balance, processing requests one at a time.

  • Completion Handlers: These notify when operations are complete, running asynchronously in the background without blocking the caller.

  • Non-blocking: By using async, the client continues executing other tasks while deposit or withdrawal operations run in the background, improving responsiveness.

This highlights how the GCD version manages concurrency without blocking client code.

Comparison with Swift Concurrency and Actors

Now, compare this to the actor-based version in Swift:

Key Differences:

  1. Concurrency Handling:

    • GCD: We manually ensure thread safety using serial queues.

    • Swift Actors: Swift automatically handles the concurrency by isolating the state within the actor.

  2. Completion Handlers vs. async/await:

    • GCD: We have to use completion handlers to notify when the asynchronous operation is finished, which can lead to more complex code (callback hell).

    • Swift Concurrency: Using async/await makes the code much more linear and readable.

  3. Boilerplate:

    • GCD: Requires setting up queues, managing execution manually, and handling completion handlers.

    • Swift Actors: System abstracts the concurrency management, so we focus only on the logic without worrying about thread safety.

This demonstrates how Swift Concurrency simplifies the handling of asynchronous tasks and concurrency management, making the code easier to read and maintain compared to the more manual GCD and completion handler approach.

Next
Next

How to migrate your code to Swift’s Concurrency - Part II