Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are Swift4 variables atomic?

Tags:

swift

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: enter image description here Output with NSLock:
[1,2,3,...,2999,3000]

like image 431
JsW Avatar asked Mar 06 '18 03:03

JsW


1 Answers

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:

  1. get the current value, store it in a CPU register
  2. increment that CPU register
  3. store the incremented value into the property

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.

like image 110
Cristik Avatar answered Oct 12 '22 14:10

Cristik