Search code examples
swiftconcurrencygrand-central-dispatch

Where and why deadlock?


I have 2 concurrent queues:

let concurrentQueue = DispatchQueue(label: "test.concurrent", attributes: .concurrent)
let syncQueue = DispatchQueue(label: "test.sync", attributes: .concurrent)

And code:

for index in 1...65 {
    concurrentQueue.async {
        self.syncQueue.async(flags: .barrier) {
            print("WRITE \(index)")
        }
        
        self.syncQueue.sync {
            print("READ \(index)")
        }
    }
}

Outputs:

WRITE 1
READ 1

Why, where and how it gets deadlock?

With <65 iterations count everything is good.


Solution

  • This pattern (async writes with barrier, concurrent reads) is known as the “reader-writer” pattern. This particular multithreaded synchronization mechanism can deadlock in thread explosion scenarios.

    In short, it deadlocks because:

    • You have “thread explosion”;

    • You have exhausted the worker thread pool, which only has 64 threads;

    • Your dispatched item has two potentially blocking calls, not only the sync, which obviously can block, but also the concurrent async (see next point); and

    • When you hit a dispatch, if there is not an available worker thread in the pool, it will wait until one is made available (even if dispatching asynchronously).

    The key observation is that one should simply avoid unbridled thread explosion. Generally we reach for tools such as GCD's concurrentPerform (a parallel for loop which is constrained to the maximum number of CPU cores), operation queues (which can be controlled through judicious maxConcurrentOperationCount setting) or Swift concurrency (use its cooperative thread pool to control degree of concurrency, actors for synchronization, etc.).


    While the reader-writer has intuitive appeal, in practice it simply introduces complexities (synchronization for multithreaded environment with yet another multithreaded mechanism, both of which are constrained by surprisingly small GCD worker thread pools), without many practical benefits. Benchmark it and you will see that it is negligibly faster than a simple serial GCD queue, and relatively much slower than lock-based approaches.