Search code examples
swiftasynchronousdispatch-queue

DispatchQueue async issue


var numbers: [Int] = []
var queue = DispatchQueue(label: "myqueue", attributes: .concurrent)

DispatchQueue.concurrentPerform(iterations: 10) { i in
    
    queue.async() {
        print(i)
        let last = numbers.last ?? 0
        numbers.append(last + 1)
    }
}

sleep(1)
print(numbers)

It output:

[1, 2, 3, 4, 5] -- (maybe different)

The work item inside the async block runs 10 times, but why there are less than 10 numbers in the array?

The second question is if I change async to sync, the numbers are still less than 10. Why is it not [1 2 3 4 5 6 7 8 9 10]?


Solution

  • Arrays in Swift are not thread safe. What would happen when you access them concurrently is undefined. So some elements not being added, or the wrong elements being added, or your program crashing, are all possible results from running the code.

    Changing to sync doesn't help, because you are still posting work on a concurrent queue, which will run them concurrently, when you post work concurrently on it using concurrentPerform.

    What sync guarantees is that it returns after the work completes, so that the code after it runs after the work is done. For example, if you have an extra print after sync:

    queue.sync {
        print(i)
        let last = numbers.last ?? 0
        numbers.append(last + 1)
    }
    print("Foo")
    

    Foo is guaranteed to be printed after numbers.append(last + 1).

    See also this guide.

    So to achieve [1,2,3,4,5,6,7,8,9,10], you should post the work on a serial queue, which guarantees that the work will be executed sequentially. Alternatively, if you only have a concurrent queue, use the .barrier flag:

    queue.async(flags: .barrier) {
    

    Also, if what you actually meant is for concurrentPerform to use queue to do the "concurrent performing", then as this post says, you should do it like this:

    queue.async {
        DispatchQueue.concurrentPerform(iterations: 10) { i in
            print(i)
            someSerialQueue.async {
                let last = numbers.last ?? 0
                numbers.append(last + 1)
            }
        }
    }