Search code examples
swiftmultithreadingthread-safety

Data racing crash on DispatchQueue.global().sync method


My code is quite straightforward. My understanding is remove operation should be thread-safe since I wrap with queue.sync

let queue = DispatchQueue.global()

var fetchingInProgressList: Set<String> = []

enter image description here


Solution

  • tl;dr

    No, you cannot use a global queue for thread-safe synchronization. A simple serial queue would, though. Or, nowadays, in Swift concurrency, we might reach for an “actor”.

    But a global queue is not sufficient to achieve thread-safety.


    As has been noted, global returns a concurrent queue. As the docs say:

    Tasks submitted to the returned queue are scheduled concurrently with respect to one another.

    And sync calls it synchronously:

    This function submits a block to the specified dispatch queue for synchronous execution … this function does not return until the block has finished.

    So, sync will block the caller until the dispatched work finishes (i.e., it runs “synchronously”). But, sync has no impact on other threads interacting with the same concurrent queue (i.e., it, alone, provides no assurances regarding “synchronization”). Work dispatched to the same global queue can run in parallel with other work dispatched to the same queue by other threads. That violates thread-safety. It is irrelevant whether it was dispatched using sync or async. The relevant question is whether a queue can prevent the race, i.e., prevent parallel execution on that queue. Global queues provide no such assurances.

    So, in short, dispatching to a global queue (whether synchronously or asynchronously) is insufficient to achieve thread-safety. There are two legacy techniques for achieving thread-safety with GCD:

    1. Use a serial dispatch queue instead of a concurrent queue. This way, neither reads nor writes can happen concurrently with respect to each other.

    2. Use a “barrier” when dispatching writes. This is called the “reader-writer” pattern. But you cannot use barriers on a global queue, so you can only do this on your own private/custom concurrent queue. (FWIW, while reader-writer has a certain intuitive appeal, I have retired it from my codebases: Where code clarity and maintainability is called for there are better patterns. In those rare computationally-intensive situations, where performance is the paramount concern, again, there are much better alternatives.)

    Regardless of which of the above you choose, the next question is whether you dispatch synchronously (sync) or asynchronously (async): This is dictated by whether the caller must wait for the dispatched work to complete. E.g., if just writing data, we may do so asynchronously, with async. However if reading data, we often would do that synchronously, with sync.

    All of that having been said, nowadays, with Swift concurrency, we would consider using actors. Actors are discussed in WWDC videos Protect mutable state with Swift actors and Eliminate data races using Swift Concurrency.


    FWIW, if you are trying to identify potential thread safety issues in GCD code, I might encourage you to temporarily turn on the Thread Sanitizer (aka TSAN) as outlined in Diagnosing memory, thread, and crash issues early: Detect data races among your app’s threads.