My code is:
import Foundation
let q1 = DispatchQueue(
label: "test-1",
qos: .userInteractive,
attributes: .concurrent
)
let q2 = DispatchQueue(label: "test-2")
var i = 0;
q1.async {
for _ in 0..<10 {
i+=1
print("async 1-1: ", i)
}
}
q1.async {
for _ in 0..<10 {
i+=1
print("async 1-2: ", i)
}
}
q2.async {
for _ in 0..<10 {
i+=1
print("async 2: ", i)
}
}
for _ in 0..<10 {
i+=1
print("sync: ", i)
}
Run it on playground, seems the write action is serial across different DispatchQueue:

Test few scenario, seems the write action is serial across different DispatchQueue. If it's concurrent, at least it should have some repeat value right?
Should I need to add lock for var i? I am new to swift, not sure my understanding on the DispatchQueue is correct.
This code is not thread-safe, and the resultant behavior will be unpredictable:
You may just get invalid results.
E.g., I did 30,000 iterations, incrementing the value, and I got a total of 29,967 (!):
Doing this 30,000 times (on my iPad, at least) was enough to manifest simple problems where the parallel loops stepped on top of each other occasionally. The problem is that the simple
i += 1is really a three-step process: 1. a “fetch” of the current value ofi, 2. incrementing that value, and 3. a “store” of the result back toi. If one thread has started this increment process, say with an initial value of42, and another starts this process itself (again, with the value of42), both of these will end up storing a value of43at the end (!). You really want to ensure that the second thread does not attempt to start incrementing the value in a particular iteration until the first thread has finished incrementing the value in one of its iterations.Now, the
i += 1is so quick that this rarely happens. This is why I had to bump the number of iterations up to consistently manifest the problem. I also remove theprintstatements inside the loops, to increase the chance of manifesting the race.You may crash.
E.g., if I let it go long enough, in a Playground I see the following:
Doing 30m operations was enough to consistently generate this low-level crash.
It may often appear to work without incident (but it is still not safe).
Doing it 30 times is simply not enough it to consistently manifest a problem. It may still occasionally return the wrong value or even crash, but just less consistently so. But as a WWDC video stated, “there is no such thing as a benign race.”
Bottom line, your results may vary based upon your hardware and your environment (e.g., a Playground vs Xcode). But you can see that data races may are hard to predict.
When using GCD, I would suggest temporarily turning on Xcode’s “thread sanitizer” (TSAN) and it will do a credible job at identifying these data races. (Playgrounds is not good environment for testing these sorts of issues. Also, rather than 10 iterations, you would likely need to do millions of iterations to consistently manifest the problem.) See Detect data races among your app’s threads section of Diagnosing memory, thread, and crash issues early. Just click the “Thread sanitizer” checkbox and run the app and you will see issues reported in the console.
In answer to your question, back in the days of GCD, yes, we would add our own synchronization. E.g., in GCD, we might use locks or make sure that we use a separate, dedicated, serial dispatch queue for all interaction with this shared mutable state. The goal is to eliminate these “data races”. And this synchronization must be around the whole “increment” process, not just the fetching and storing of
i(or else you will still get the problem illustrated in point 1, above).Nowadays, we might use Swift concurrency instead. In Swift concurrency, we eliminate data races with “actors”. See WWDC videos Protect mutable state with Swift actors and Eliminate data races using Swift Concurrency. And if you are unfamiliar with Swift concurrency, see Meet async/await in Swift, as well as the other videos linked on that page.