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?
CodePudding user response:
Thanks to Rob's answer, here is how to fix this properly:
import Foundation
actor AnimalActor {
var animal = "Dog"
func setAnimal(newAnimal: String) {
animal = newAnimal
}
}
func testAsync() async {
let animalActor = AnimalActor()
DispatchQueue.main.asyncAfter(deadline: .now() 2) {
Task {
await animalActor.setAnimal(newAnimal: "Cat")
print(await animalActor.animal)
}
}
print(await animalActor.animal)
}
Task {
await testAsync()
}
CodePudding user response:
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.