Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 671
Jere Avatar asked Sep 07 '25 12:09

Jere


2 Answers

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.

like image 67
Rob Avatar answered Sep 10 '25 12:09

Rob


First of all, if you can, use structured concurrency, as the other answers suggest.

I hit a case where there is no clean structured concurrency API: A protocol that requires to return a value non-async.

protocol Proto {
    func notAsync() -> Value
}

To compute the Value, async method calls are needed. I settled for this solution:

func someAsyncFunc() async -> Value {
    ...
}

class Impl: Proto {
    func notAsync() -> Value {
        return UnsafeTask {
            await someAsyncFunc()
        }.get()
    }
} 

class UnsafeTask<T> {
    let semaphore = DispatchSemaphore(value: 0)
    private var result: T?
    init(block: @escaping () async -> T) {
        Task {
            result = await block()
            semaphore.signal()
        }
    }

    func get() -> T {
        if let result = result { return result }
        semaphore.wait()
        return result!
    }
}

You can copy past the UnsafeTask class and use it in your code if you hit the same case.

I consider this quite an ugly solution, eg: The type needs to be a class because structs get concurrency-checked, which means that the compiler errors on the concurrent access to both semaphore and result. As far as I know, semaphore should be thread safe and the result is only written to from one context and read from by the rest. In case T is pointer-sized or smaller, the write is atomic and thus 'safe'. In other cases it may not be safe. Though I might be overlooking some concurrency edge case. Open for suggestions.

like image 43
Berik Avatar answered Sep 10 '25 12:09

Berik