5

I believe XCode is incorrectly reporting Swift Access Race in my SynchronizedDictionary - or is it?

My SynchronizedDictionary looks like this:

public struct SynchronizedDictionary<K: Hashable, V> {
    private var dictionary = [K: V]()
    private let queue = DispatchQueue(
        label: "SynchronizedDictionary",
        qos: DispatchQoS.userInitiated,
        attributes: [DispatchQueue.Attributes.concurrent]
    )

    public subscript(key: K) -> V? {
        get {
            return queue.sync {
                return self.dictionary[key]
            }
        }
        mutating set {
            queue.sync(flags: .barrier) {
                self.dictionary[key] = newValue
            }
        }
    }
}

The following test code will trigger a "Swift Access Race" issue (when the Thread Sanitizer is turned on for the scheme):

var syncDict = SynchronizedDictionary<String, String>()

let setExpectation = XCTestExpectation(description: "set_expectation")
let getExpectation = XCTestExpectation(description: "get_expectation")

let queue = DispatchQueue(label: "SyncDictTest", qos: .background, attributes: [.concurrent])

queue.async {
    for i in 0...100 {
        syncDict["\(i)"] = "\(i)"
    }
    setExpectation.fulfill()
}

queue.async {
    for i in 0...100 {
        _ = syncDict["\(i)"]
    }
    getExpectation.fulfill()
}

self.wait(for: [setExpectation, getExpectation], timeout: 30)

The Swift Race Access look like this:

Swift Race Access I really did not expect there to be an access race condition here, because the SynchronizedDictionary should handle the concurrency.

I can fix the issue by, in the test, wrapping the getting and setting in a DispatchQueue similar to the actual implementation of the SynchronizedDictionary:

let accessQueue = DispatchQueue(
    label: "AccessQueue",
    qos: DispatchQoS.userInitiated,
    attributes: [DispatchQueue.Attributes.concurrent]
)

var syncDict = SynchronizedDictionary<String, String>()

let setExpectation = XCTestExpectation(description: "set_expectation")
let getExpectation = XCTestExpectation(description: "get_expectation")

let queue = DispatchQueue(label: "SyncDictTest", qos: .background, attributes: [.concurrent])

queue.async {
    for i in 0...100 {
        accessQueue.sync(flags: .barrier) {
            syncDict["\(i)"] = "\(i)"
        }
    }
    setExpectation.fulfill()
}

queue.async {
    for i in 0...100 {
        accessQueue.sync {
            _ = syncDict["\(i)"]
        }
    }
    getExpectation.fulfill()
}

self.wait(for: [setExpectation, getExpectation], timeout: 30)

...but that already happens inside the SynchronizedDictionary - so why is Xcode reporting an Access Race Condition? - is Xcode at fault, or am I missing something?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Pelle Stenild Coltau
  • 1,268
  • 10
  • 15
  • Your 2 async blocks can be assigned to different threads controlled by your concurrent queue. Either of those threads could execute first. I suspect it's therefore complaining that the code doesn't guarantee the setter will run before the getter. You can test my theory by making it a serial queue and seeing whether the error goes away. – Phillip Mills Mar 07 '19 at 01:49
  • Thank you for your feedback. I agree with you, that replacing the concurrent queue with a serial will fix the issue. However, that defies the purpose of the SynchronizedDictionary - being able to access it concurrently. – Pelle Stenild Coltau Mar 20 '19 at 15:13
  • 1
    @PhillipMills In the end, both the setter and getter goes into the concurrent queue and the setter blocks everything. I don't see how this results in a race conditions when everything is blocked and waits for the setter to complete. – J. Doe Mar 26 '19 at 12:11
  • 1
    @PelleStenildColtau: Note that in the setter you can dispatch *asynchronously* (with a barrier), see for example https://medium.com/@oyalhi/dispatch-barriers-in-swift-3-6c4a295215d6. – Martin R Mar 28 '19 at 08:17

1 Answers1

5

The thread sanitizer reports a Swift access race to the

var syncDict = SynchronizedDictionary<String, String>()

structure, because there is a mutating access (via the subscript setter) at

syncDict["\(i)"] = "\(i)"

from one thread, and a read-only access to the same structure (via the subscript getter) at

_ = syncDict["\(i)"]

from a different thread, without synchronization.

This has nothing to do with conflicting access to the private var dictionary property, or with what happens inside the subscript methods at all. You'll get the same “Swift access race” if you simplify the structure to

public struct SynchronizedDictionary<K: Hashable, V> {
    private let dummy = 1

    public subscript(key: String) -> String {
        get {
            return key
        }
        set {
        }
    }
}

So this is a correct report from the thread sanitizer, not a bug.

A possible solution would be to define a class instead:

public class SynchronizedDictionary<K: Hashable, V> { ... }

That is a reference type and the subscript setter no longer mutates the syncDict variable (which is now a “pointer” into the actual object storage). With that change, your code runs without errors.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Ah so it's a race condition to the variable `syncDict` and this has nothing to do with whatever is inside the `syncDict` at all :) – J. Doe Mar 27 '19 at 21:25
  • @MartinR, I recently had this issue, and although I changed my SynchorizedDictionary to a class I don't fully understand why it worked. I decided to google it and then I stumbled upon your answer. I understand that the getter and setter can be accessed from different threads but the body of those are synchronized. So I don't understand why XCode is complaining about it and why I occasionally saw crashes at run time. – Anwuna Mar 09 '23 at 20:45