DispatchSerialQueue apppears to execute blocks Concurrently

Every piece of documentation and tutorial I can find says that there are concurrent and serial dispatch queues, that the latter is the default and will execute tasks FIFO one at a time. This is the documented behavior if you use sync() or async() to submit blocks. Just from Apple's documentation:

However! My serial queues are causing and allowing race conditions. they appear to allow blocks to execute concurrently. Here are my simple tests. The severity of the difference between the expected and actual values depends on the time each block takes to work.

import Foundation

func simpleMutation(_ state: Int) -> Int {
    return state + 1
}
func castingMutation(_ state: Any) -> Any {
    let intState = state as! Int
    return intState + 1
}

func dictIntState() {
    var state = ["test":0]
    let Q = DispatchQueue(label: "dict int state",  target: DispatchQueue.global())
    
    func enqueue() {
        Q.async(){
            //pull out of dict, mutate, and push back in
            state["test"] = simpleMutation(state["test"]!)
        }
    }
    for _ in 1...100000 {
        enqueue()
    }
    print("expected: 100000 | actual: \(state["test"])")
}
func dictAnyState() {
    var state: [String: Any] = [:]
    state["test"] = 0
    let Q = DispatchQueue(label: "dict int state", target: DispatchQueue.global())

    func enqueue() {
        Q.async(){
            state["test"] = castingMutation(state["test"]!)
        }
    }
    for _ in 1...100000 {
        enqueue()
    }
    print("expected: 100000 | actual \(state["test"]!)")
}
func intState() {
    var state = 0
    let Q = DispatchQueue(label: "dict int state", target: DispatchQueue.global())

    func enqueue() {
        Q.async(){
            state = simpleMutation(state)
        }
    }
    for _ in 1...100000 {
        enqueue()
    }
    print("expected: 100000 | actual \(state)")
}

func intStateSlowMutation() {
    var state = 0
    let Q = DispatchQueue(label: "dict int state", target: DispatchQueue.global())

    func enqueue() {
        Q.async(){
            state = simpleMutation(state)
            Thread.sleep(forTimeInterval: 10)
        }
    }
    for _ in 1...1000 {
        enqueue()
    }
    print("expected: 1000 | actual \(state)")
}


print("Starting tests, this may take a while...")
dictIntState()
dictAnyState()
intState()
intStateSlowMutation()
print("testing Over!")

What gives? Is this the intended behavior and I'm missing something? If so, what is meant to be the difference between a serial and concurrent queue?

Any help would be much appreciated.

P.S. Why not use Swift Actors for serialization? Actors don't play nice with some of the creative state-management stuff we're trying to do. Which, fun topic!

Enqueue your prints so they run at the very end:

Q.async {
    print(...)
}
2 Likes

Oh obviously. Yes. Thank you. I swear I read my code before sending this.
We were having a similar issue with a more complex setup, blocks being added from more contexts and I drafted this contrived test for it. I'll keep adding stuff until the problem manifests again.