Why could this code snippet potentially lead to a data race involving someArray?
From a technical perspective, we are not accessing the same resource in its entirety (i.e., the full array or its underlying buffer), but rather distinct memory locations—specifically, individual 64-bit (Int64) elements within the array. Even if the element size were smaller than a machine word, the memory layout would typically prevent two elements from being accessed in a single operation, which suggests that concurrent access to separate elements might be safe.
However, the Swift 6 compiler produces the following warning: “Main actor-isolated var ‘someArray’ cannot be mutated from a nonisolated context.” I know that Arrays are not thread-safe, but I would like to understand the underlying mechanism. Maybe the for-loop scope captures the the entire array
I would appreciate clarification on the expected behavior in this scenario and whether it could, in fact, result in a data race. I tend to think that the behavior may vary across different Swift releases.
Swift arrays use copy-on-write for mutations, meaning that any mutation (even subscripts) can end up changing the whole array (by copying it to a new array buffer). Details aside, that would be a race on the someArrayvariable whether or not it races on the array contents.
Now, you can say that in practice this won’t happen, because all your accesses are from one queue. But the compiler doesn’t do that kind of global reasoning.
var someArray = [1,2,3]
let originalArray = someArray // oops, now there are two references
for i in 0..<someArray.count {
cq.async {
someArray[i] = 0
}
}
But I think COW is thread safe, right? I wrote an equivalent version using Swift concurrency and it compiles (note the var b = a line). I wonder what's the underlying difference between DispatchQueue version and my version? Does this example indicate Swift concurrency is more powerful?
@concurrent
func perform(_ fn: sending () -> Void) async {
fn()
}
@MainActor
func test() async {
var a = [1, 2, 3]
var b = a
for i in 0..<a.count {
await perform {
a[i] = 0
}
}
}
@globalActor actor MyGlobalActor {
static let shared = MyGlobalActor()
}
@MyGlobalActor
func test() async {
var a = [1, 2, 3]
var b = a
for i in 0..<a.count {
await perform { @MainActor in
a[i] = 0
}
}
}
@MainActor
func perform(_ fn: @MainActor () -> Void) async {
fn()
}
Your version doesn’t run in parallel, because you have an await inside the loop. So it’s closer to queue.sync rather than queue.async. If you try with TaskGroup you should see the same error.
Thank you for your response. The explanation regarding the subscript is clear now.
However, I would appreciate some clarification about the data race. You mentioned that a data race would not occur because all accesses happen on a single queue. Since the queue is concurrent, it can execute multiple tasks simultaneously on different threads. Wouldn’t that make simultaneous mutations from different threads possible?