I was wondering if Swift 4 variables are atomic or not. So I did the following test.
The following is my test code.
class Test {
var count = 0
let lock = NSLock()
func testA() {
count = 0
let queueA = DispatchQueue(label: "Q1")
let queueB = DispatchQueue(label: "Q2")
let queueC = DispatchQueue(label: "Q3")
queueA.async {
for _ in 1...1000 {
self.increase()
}
}
queueB.async {
for _ in 1...1000 {
self.increase()
}
}
queueC.async {
for _ in 1...1000 {
self.increase()
}
}
}
///The increase() method:
func increase() {
// lock.lock()
self.count += 1
print(count)
// lock.unlock()
}
}
The output is as following with lock.lock()
and lock.unlock()
commented.
3
3
3
4
5
...
2999
3000
The output is as following with lock.lock()
and lock.unlock
uncommented.
1
2
3
4
5
...
2999
3000
My Problem
If the count
variable is nonatomic, the queueA, queueB and the queueC should asynchronous call the increase()
, which is resulted in randomly access and print count
.
So, in my mind, there is a moment, for example, queueA and queueB got count
equal to like 15, and both of them increase count
by 1 (count += 1
), so the count should be 16 even though there are two increasements executed.
But the three queues above just randomly start to count at the first beginning, then everything goes right as supposed to.
To conclude, my question is why count
is printed orderly?
Update:
The problem is solved, if you want to do the experiment as what I did, do the following changes.
1.Change the increase()
to the below, you will get reasonable output.
func increase() {
lock.lock()
self.count += 1
array.append(self.count)
lock.unlock()
}
2.The output method:
@IBAction func tapped(_ sender: Any) {
let testObjc = Test()
testObj.testA()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+3) {
print(self.testObj.array)
}
}
Output without NSLock:
Output with NSLock:[1,2,3,...,2999,3000]
No, Swift properties are not atomic by default, and yes, it's likely you'll run into multi-threading issues, where multiple threads use an outdated value of that property, a property which just got updated.
But before we get to the chase, let's see what an atomic property is.
An atomic property is one that has an atomic setter - i.e. while the setter does it's job other threads that want to access (get or set) the property are blocked.
Now in your code we are not talking about an atomic property, as the +=
operation is actually split into at least three operations:
And even if the setter would be atomic, we can end up in situation where two threads "simultaneously" reach #1 and try to operate on the same value.
So the question here should be: is increase()
an atomic operation?
Now back to the actual code, it's the print
call that "rescues" you. An increment-and-store operation takes a very short amount of time, while printing takes much longer. This is why you seem do not run into race conditions, as the window where multiple threads can use an outdated value is quite small.
Try the following: uncomment the print
call also, and print the count
value after an amount time larger enough for all background threads to finish (2 seconds should be enough for 1000 iterations):
let t = Test()
t.testA()
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// you're likely to get different results each run
print(t.count)
}
RunLoop.current.run()
You'll see now that the locked version gives consistent results, while the non-locked one doesn't.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With