Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Thread dispatching and Actors: understanding execution

Actors ensure your code is executed on a specific thread, like the main or a background thread. They help you synchronize access to mutable states and prevent data races. However, developers commonly misunderstand how actors dispatch to threads in non-async contexts. It’s an essential understanding to avoid unexpected crashes.

Before we dive deeper into the specifics of dispatching, I encourage you to read my articles Actors in Swift: how to use and prevent data races and MainActor usage in Swift explained to dispatch to the main thread since they will introduce you to the concept of actors in general. In this article, we’ll explore the effect of calling methods marked with any actor attribute.

Thread dispatching in asynchronous contexts

Let’s start with the most common dispatching when working with actors. In general, you’ll likely going to use actors in asynchronous environments. In case your calling code accesses actor-attributed methods, you’ll have to use a task or adopt the same global actor:

The compiler will prevent dispatching to an actor's thread from a non-async context.
The compiler will prevent dispatching to an actor’s thread from a non-async context.

The above example image demonstrates the real power of Swift’s concurrency framework and the compiler. You’ll be instructed to adjust your code when you’re trying to access asynchronous code from a non-async context.

The example DispatchCoordinator contains a dispatch() method that isn’t marked with the same actor attribute as the ActorDispatcher method. Therefore, we need to either use a task in combination with an await:

struct DispatchCoordinator {
    static func dispatch() {
        Task {
            let dispatcher = ActorDispatcher()
            await dispatcher.methodAttributedWithMainActor()
        }
    }
}

Or we need to add the same actor attribute to either the method or the enclosing DispatchCoordinator instance:

struct DispatchCoordinator {
    @MainActor
    static func dispatch() {
        let dispatcher = ActorDispatcher()
        dispatcher.methodAttributedWithMainActor()
    }
}

The above should make sense in most cases, as the compiler will instruct you accordingly. However, there are cases in which you won’t get any instructions.

Stay updated with the latest in Concurrency

The 2nd largest newsletter in the Apple development community with 18,598 developers. Don't miss out – Join today:


Thread dispatching in synchronous contexts

A common mistake I’ve seen many developers make (including myself) is assuming a method is always executed on the main thread when attributed to @MainActor. For example, you might have existing code that dispatches to a background thread using DispatchQueue:

DispatchQueue.global().async {

    /// Executed on some kind of background thread.
    /// In this case, simulated by calling into DispatchQueue.global().
    let dispatcher = ActorDispatcher()
    dispatcher.methodAttributedWithMainActor()
}

The above is an explicit example, but many APIs from Apple in frameworks like Foundation return on a background thread without you even noticing. The code compiles, and the compiler is not suggesting any changes regarding using a task or similar. Therefore, we assume our code is thread-safe and executes nicely on the main thread. The opposite is true when pausing our app and looking into the Debug Navigator:

The code executes on a background thread, even though it's attributed to @MainActor.
The code executes on a background thread, even though it’s attributed to @MainActor.

The above DispatchQueue closure executes synchronously in a nonisolated context. The compiler can only suggest changes when it’s confident about potential failures and doesn’t enforce actor isolation in code that’s not concurrency aware. In other words, we can’t assume our code executes on the destination actor in case there are no compilation failures.

Strict Concurrency Checking to the rescue

Luckily, we won’t be able to compile the above code when Swift 6 arrives. You can already opt-in to Strict Concurrency Checking with the latest Xcode by changing the build setting:

The strict concurrency checking build setting prepares you for Swift 6.0.
The strict concurrency checking build setting prepares you for Swift 6.0.

Once enabled, you’ll notice the following warning after compiling:

Call to main actor-isolated instance method ‘methodAttributedWithMainActor()’ in a synchronous nonisolated context; this is an error in Swift 6

The warning would have helped us prevent the unexpected thread destination. It’s essential to prepare your projects for Swift 6 and prevent yourself from having to fix many of these warnings in the future. If you want to learn more, I encourage you to read Swift 6: Preparing your Xcode projects for the future.

Continuing your journey into Swift Concurrency

The concurrency changes are more than imports and include many new features you can benefit from in your code. Now that you’ve learned about pre-concurrency maintenance, it’s time to dive into other concurrency features:

Conclusion

While actors are an excellent solution for Swift Concurrency, they can lead to confusion and wrong expectations. Knowing what to expect from the compiler and when you can be sure about a thread destination is essential to prevent runtime exceptions.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.

Thanks!