1

I have a class that reads from and writes to a dictionary using a serial dispatch queue. While the write operations are asynchronous, the read operations are synchronous. I want to avoid the use of a completion handler for reading. Does this create any issues? The issue could range from deadlocks or crashes by calling from the main thread.

public final class UserRegistry {

    private var infoMap: [String: User] = [:]

    
    private let queue: DispatchQueue = .init(label: "userRegistry.queue")

    func register(_ user: User) {
        queue.async { [weak self] in
            self?.infoMap[user.id] = user
        }
    }

    func deregister(_ user: User) {
        queue.async { [weak self] in
            self?.infoMap[user.id] = nil
        }
    }

    func getUser(for id: String) -> User? {
        queue.sync {
            infoMap[id]
        }
    }
}

I tried to create a completion handler and that would cause a series of changes at the call site across the code base. I expect that reading synchrnously should be safe regardless of which thread the consumer calls this from.

Read operation - func getUser(for id: String) -> User?

Write operations - func register(_ user: User) & func deregister(_ user: User)

DesperateLearner
  • 1,115
  • 3
  • 19
  • 45
  • "While the write operations are asynchronous the write operations are synchronous"; which one is it? – l'L'l Mar 04 '23 at 01:26
  • Safe in what regard? Against concurrent reads/writes to the dictionary or against possible deadlocks? Why async writes? `sync` writes would work too and would avoid lots of difficult to debug subtle possible bugs. – HangarRash Mar 04 '23 at 01:28
  • Safe against possible deadlocks and crashes if called from main thread – DesperateLearner Mar 04 '23 at 01:29
  • Why not use a concurrent queue but make all of the writes use the `.barrier` flag? This provides serial writes and concurrent reads. More efficient. – HangarRash Mar 04 '23 at 01:31
  • Look into async await – lorem ipsum Mar 04 '23 at 01:40
  • DispatchQueue is not Swift Concurrency. They are opposites. – matt Mar 04 '23 at 01:51
  • @l'L'l - This pattern of synchronous reads and asynchronous writes is a very common pattern. The question isn’t “which one is it?”, but rather “just because you want synchronous reads, why must writes be synchronous, too (thereby unnecessarily blocking the caller)?” – Rob Mar 05 '23 at 22:26
  • 1
    @Rob, My comment was in relation to the OP's original question. They have sine changed it to "While the write operations are asynchronous, the read operations are synchronous". It was unclear before, cheers! – l'L'l Mar 06 '23 at 22:04
  • DesperateLearner, the short answer to “Is it safe to call `DispatchQueue` `sync` for read operations?” is that, if the call is guaranteed to always be fast (measured in a few milliseconds), then yes, you can use `sync`. Otherwise, no. But this is one of the few scenarios (the quick synchronization of some variable to ensure thread-safety) where you can use `sync`. Otherwise, we would generally avoid `sync`. But this is a perfectly reasonable use-case. – Rob Mar 10 '23 at 21:42

1 Answers1

2

Yes, you can safely use sync for reads and async for writes and there will not be any deadlocks or races on infoMap.

You don't even particularly need to use [weak self], since the blocks capturing self won't live long.

You also don't probably need to use async for the writes. You could just use sync for writes too. Because infoMap is never copied, there won't be any copy-on-writes, and it will only be reallocated rarely. Thus the write operations should almost always be cheap enough to be run synchronously.

Keep in mind that if User is a reference type (or otherwise doesn't have value semantics), you might still have races involving individual User objects.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848