Search code examples
swiftasync-awaitxcode14

Mutation of captured var in concurrently-executing code


I had an issue in Swift 5.5 and I don't really understand the solution.

import Foundation

func testAsync() async {

    var animal = "Dog"

    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        animal = "Cat"
        print(animal)
    }

    print(animal)
}

Task {
    await testAsync()
}

This piece of code results in an error

Mutation of captured var 'animal' in concurrently-executing code

However, if you move the animal variable away from the context of this async function,

import Foundation

var animal = "Dog"

func testAsync() async {
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        animal = "Cat"
        print(animal)
    }

    print(animal)
}

Task {
    await testAsync()
}

it will compile. I understand this error is to prevent data races but why does moving the variable make it safe?


Solution

  • Regarding the behavior of the globals example, I might refer you to Rob Napier’s comment re bugs/limitations related to the sendability of globals:

    The compiler has many limitations in how it can reason about global variables. The short answer is “don't make global mutable variables.” It‘s come up on the forums, but hasn‘t gotten any discussion. https://forums.swift.org/t/sendability-checking-for-global-variables/56515

    FWIW, if you put this in an actual app and change the “Strict Concurrency Checking” build setting to “Complete” you do receive the appropriate warning in the global example:

    Reference to var 'animal' is not concurrency-safe because it involves shared mutable state

    This compile-time detection of thread-safety issues is evolving, with many new errors promised in Swift 6 (which is why they’ve given us this new “Strict Concurrency Checking” setting so we can start reviewing our code with varying levels of checks).

    Anyway, you can use an actor to offer thread-safe interaction with this value:

    actor AnimalActor {
        var animal = "Dog"
        
        func setAnimal(newAnimal: String) {
            animal = newAnimal
        }
    }
    
    func testAsync() async {
        let animalActor = AnimalActor()
        
        Task {
            try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
            await animalActor.setAnimal(newAnimal: "Cat")
            print(await animalActor.animal)
        }
    
        print(await animalActor.animal)
    }
    
    Task {
        await testAsync()
    }
    

    For more information, see WWDC 2021’s Protect mutable state with Swift actors and 2022’s Eliminate data races using Swift Concurrency.


    Note, in the above, I have avoided using GCD API. The asyncAfter was the old, GCD, technique for deferring some work while not blocking the current thread. But the new Task.sleep (unlike the old Thread.sleep) achieves the same behavior within the concurrency system (and offers cancelation capabilities). Where possible, we should avoid GCD API in Swift concurrency codebases.