Swift Concurrency Migration Guide

repository·main·Indexed 18 days ago

https://github.com/swiftlang/swift-migration-guide

Guidance for developers transitioning Swift code to modern concurrency features, specifically for Swift 6 mode. Covers resolving data races, fixing unsafe global and static variables, addressing non-Sendable global reference types, and resolving protocol conformance isolation mismatches using @MainActor, @preconcurrency, and nonisolated keywords.

Tokens
14.7K
Snippets
46
Records
72
Agent score
63%

What's inside Swift Concurrency Migration Guide

  1. Migrating to the Swift 6 language mode

    main

    The Swift 6 language mode enables strict compiler safety checks to guarantee that concurrent programs are free of data races. This mode is opt-in and can be enabled on a per-target basis, allowing for incremental migration.

    It is important to distinguish between the compiler version and the language mode. The Swift 6 compiler supports four distinct language modes:

    • 6 (Full Swift 6 safety checks)
    • 5
    • 4.2
    • 4

    Targets using previous language modes or code in other languages can interoperate with modules migrated to the Swift 6 language mode.

  2. How Main actor annotations affect compatibility

    main

    Adding @MainActor to protocols, types, or functions is generally source and ABI incompatible.

    Protocols and types

    Annotating a protocol or type with @MainActor is incompatible because the annotation can be inferred throughout client code (subclasses, extensions, etc.).

    To resolve: Apply @preconcurrency to the declaration. However, if the annotation is inferred on extension methods in client code, @preconcurrency might not preserve ABI. In those cases, you may need to explicitly mark extension methods as nonisolated in the client library.

    // In the client library, if P is retroactively @MainActor:
    extension P {
      nonisolated public func onChange(action: @escaping @Sendable () -> Void)
    }

    Function declarations and types

    Adding @MainActor to a function or a function type is incompatible.

    To resolve: Apply @preconcurrency to the enclosing function declaration.

    // Source and ABI incompatible
    @MainActor public func runOnMain()
    
    // Resolved with @preconcurrency
    @preconcurrency @MainActor
    public func runOnMain() { ... }
  3. Ways to achieve Sendable conformance

    main

    There are four primary ways to make a type Sendable:

    1. Global Isolation: Mark the type with an actor (e.g., @MainActor). Accesses from other domains must be asynchronous.
    2. Actors: Actors have implicit Sendable conformance. They provide their own isolation domain, allowing them to work with non-Sendable types internally.
    3. Manual Synchronization: Use @unchecked Sendable for types that use existing synchronization primitives like DispatchQueue or locks.
    4. Retroactive Conformance: Use extension Type: @retroactive @unchecked Sendable for types in dependencies. Use with extreme caution as it can break API contracts if the type isn't actually thread-safe.
    // Manual Synchronization
    class Style: @unchecked Sendable {
        private var background: ColorComponents
        private let queue: DispatchQueue
    }
    
    // Retroactive Conformance
    extension ColorComponents: @retroactive @unchecked Sendable {}
  4. How Sendable conformance affects compatibility

    main

    Adding Sendable conformance to types and generic requirements has different impacts on compatibility.

    Conformances on concrete types

    Adding Sendable to a concrete type (including conditional conformances) is typically source and ABI compatible.

    // Source and ABI compatible
    public struct S: Sendable

    Generic requirements

    Adding a Sendable requirement to a generic type or function is source and ABI incompatible because it restricts the types clients can pass.

    To resolve: Use @preconcurrency on the declaration to downgrade failures to warnings and preserve ABI.

    // Source and ABI incompatible
    public func generic<T> where T: Sendable
    
    // Resolved with @preconcurrency
    @preconcurrency
    public func generic<T> where T: Sendable { ... }

    Function types

    Adding @Sendable to a function type is source and ABI incompatible.

    To resolve: Apply @preconcurrency to the enclosing function declaration.

    // Source and ABI incompatible
    public func performConcurrently(completion: @escaping @Sendable () -> Void)
    
    // Resolved with @preconcurrency
    @preconcurrency
    public func performConcurrently(completion: @escaping @Sendable () -> Void) { ... }
  5. Use dynamic isolation for internal-only actor requirements

    main

    If a type logically requires @MainActor isolation but adding the static annotation would break unmigrated clients, use Internal-Only Isolation. This involves keeping the class unannotated but using MainActor.assumeIsolated inside its methods to interact with @MainActor state.

    Warning: This is a temporary solution. The type's true isolation requirements remain invisible to clients, and you should eventually move to static isolation.

    class WindowStyler {
        @MainActor
        private var backgroundColor: ColorComponents
    
        func applyStyle() {
            MainActor.assumeIsolated {
                // use and interact with other `MainActor` state
            }
        }
    }
  6. Understand Isolation Inference and Inheritance

    main

    Swift uses isolation inference to establish isolation implicitly based on context.

    Classes

    • Subclasses: A subclass always inherits the same isolation as its parent. This isolation cannot be changed by the subclass.
    • Members: The static isolation of a type is automatically inferred for its properties and methods.

    Protocols

    Protocol conformance can affect isolation depending on how it is applied:

    • Type-level conformance: The inferred isolation applies to the entire type.
    • Extension-level conformance: The inferred isolation only applies within the specific extension.
    • Requirement-level isolation: Protocol requirements themselves can be marked with isolation (e.g., @MainActor func eat()).

    Function Types and Closures

    • Closures: By default, closures are isolated to the same context in which they are formed. This allows them to capture state or call isolated methods from the surrounding context.
    • Task Inheritance: The Task initializer captures the static isolation of its enclosing scope. A task will inherit the isolation of its context (e.g., MainActor) unless an explicit global actor is provided.
    @MainActor
    class Animal {
        let name: String
        func eat(food: Pineapple) {}
    }
    
    class Chicken: Animal {} // Chicken is also @MainActor
    
    @MainActor
    protocol Feedable {
        func eat(food: Pineapple)
    }
    
    class Chicken: Feedable {} // Entire type is @MainActor
    
    extension Pirate: Feedable {} // Only the extension is @MainActor
    
    @MainActor
    func eat(food: Pineapple) {
        Task {
            // Inherits MainActor isolation
            Chicken.prizedHen.eat(food: food)
        }
    
        Task { @MyGlobalActor in
            // Explicitly isolated to MyGlobalActor
        }
    }
  7. Use Asynchronous Requirements to Resolve Isolation Mismatches

    main

    Changing a synchronous protocol requirement to an async requirement provides more flexibility. An async requirement can be satisfied by an isolated method (e.g., a @MainActor method) because the caller is forced to call the method asynchronously, allowing the implementation to switch actors before accessing isolated state.

    Warning: Changing a method to async can have significant ripple effects, requiring call sites to use await and potentially requiring changes to parameter and return value types to handle crossing isolation boundaries.

    protocol Styler {
        func applyStyle() async
    }
    
    @MainActor
    class WindowStyler: Styler {
        // This matches a non-isolated async requirement
        func applyStyle() {
        }
    }
  8. Use Non-isolated declarations

    main

    By default, functions and variables are non-isolated. Non-isolated code has no specific isolation domain, meaning it cannot mutate state protected in other domains. However, non-isolated entities are always safe to access from any other domain because they do not hold protected mutable state.

    func sailTheSea() {
    }
    
    class Chicken {
        let name: String
        var currentHunger: HungerLevel
    }
  9. Use dynamic isolation for usage-only actor requirements

    main

    If you cannot contain isolation within a type, you can apply static isolation to the type and use dynamic isolation at the call sites. This allows you to expand isolation to cover only specific API usage patterns during migration.

    @MainActor
    class WindowStyler {
        // ...
    }
    
    class UIStyler {
        @MainActor
        private let windowStyler: WindowStyler
        
        func applyStyle() {
            MainActor.assumeIsolated {
                windowStyler.applyStyle()
            }
        }
    }
  10. How sending parameters and results affect compatibility

    main

    The sending keyword manages how non-sendable values are transferred across isolation boundaries.

    Result types

    Adding sending to a return type lifts restrictions in client code and is source and ABI compatible.

    // Source and ABI compatible
    public func getValue() -> sending NotSendable

    Parameter types

    Adding sending to a parameter is more restrictive for the caller and is source and ABI incompatible.

    // Source and ABI incompatible
    public func takeValue(_: sending NotSendable)

    Replacing @Sendable with sending

    Replacing an existing @Sendable closure parameter with sending is source compatible but ABI incompatible because it changes name mangling.

    To resolve: Use @_silgen_name to preserve mangling. For all functions except initializers, use __shared sending to preserve the ownership convention.

    // For standard functions
    public func takeValue(_: __shared sending NotSendable)
    
    // For initializers (no extra modifier needed)
    public class C {
      public init(ns: sending NotSendable)
    }