Explain how MainActor and Sendable interact

Asked on 2024-07-30

1 search

MainActor and Sendable are two important concepts in Swift that help manage concurrency and ensure data-race safety.

MainActor

The MainActor is a special actor in Swift that ensures certain pieces of code run on the main thread. This is particularly useful for UI updates, which must be performed on the main thread to avoid inconsistencies and crashes. When a method or property is isolated to the MainActor, it means that any access to it must be done on the main thread. This isolation helps prevent data races by serializing access to the shared state.

For example, if you have a method that needs to run on the main thread, you can annotate it with @MainActor:

@MainActor
func updateUI() {
    // UI update code
}

Sendable

Sendable is a protocol in Swift that ensures a type can be safely transferred across concurrency domains. A type is considered Sendable if it protects its state from concurrent accesses, either by being immutable or by using synchronization mechanisms like locks.

In Swift 6, the compiler enforces that values shared between concurrency domains must be Sendable. This helps prevent data races by ensuring that only safe types are passed between different threads or actors.

For example, if you have a struct that you want to make Sendable, you can conform it to the Sendable protocol:

struct MyData: Sendable {
    let value: Int
}

Interaction between MainActor and Sendable

When you have a type that is isolated to the MainActor and you need to send it to another actor, it must conform to Sendable to ensure data-race safety. If the type is a value type with immutable properties, it can be easily made Sendable. However, if it is a reference type, you need to ensure that it does not introduce shared mutable state that could lead to data races.

For example, consider a scenario where you have a Drink type that is a struct with immutable properties. This type can be made Sendable and safely passed between the MainActor and another actor:

struct Drink: Sendable {
    let name: String
    let size: Int
}

If Drink were a reference type, you would need to ensure it is thread-safe before making it Sendable.

Example from WWDC

In the session Migrate your app to Swift 6, it is explained how to handle a scenario where a Drink type is stored in one actor and then sent to another actor. By making Drink conform to Sendable, you ensure that it can be safely transferred between actors without introducing data races.

For more details, you can refer to the sessions:

Relevant Sessions

  1. Migrate your app to Swift 6
  2. A Swift Tour: Explore Swift’s features and design
  3. What’s new in Swift