Reader-writer with a barrier vs an actor

Concurrency Mid-level Swift 5.0+ iOS 17+ Reviewed By Alok Singh

Spot the bug

value(for:) always returns nil. Tap the line that causes it.

  1. final class Store: @unchecked Sendable {
  2. private var values: [String: Int] = [:]
  3. private let queue = DispatchQueue(label: "store", attributes: .concurrent)
  4. ​
  5. func value(for key: String) -> Int? {
  6. var result: Int?
  7. queue.async { result = self.values[key] }
  8. return result
  9. }
  10. ​
  11. func set(_ value: Int, for key: String) {
  12. queue.async(flags: .barrier) { self.values[key] = value }
  13. }
  14. }
Hint 1 of 2

Does async wait for the closure?

Hint 2 of 2

Which line produces the return value?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 7 queue.async { result = self.values[key] }

Line 7 reads with async, so the function returns before the closure runs; result is still nil. Reads use queue.sync { values[key] }. The Swift 6 compiler also warns about mutating a captured var in concurrently-executing code.

30-second answer

On a concurrent queue, reads run with sync so many can happen at once, and writes run with async(flags: .barrier) so each write waits for running reads and runs alone. It gives fast concurrent reads with safe writes. In new code an actor is simpler and the compiler checks it, though callers must await.

Practise saying it · 45 s. No recording, just a timer

Explain the reader-writer barrier pattern.

Say your answer out loud, then check which of these points you covered:

  • Concurrent queue
  • Reads with sync
  • Writes with async + barrier
  • Actor is simpler in new code

Deep dive

Barriers only work on a private concurrent queue you created, not global queues.

Reads must return values, so they use sync; writes can be async.

Never call the barrier write with sync from a read on the same queue.

An actor serializes reads too, which is usually fine at app scale.

Correct reader-writer Type-checked · Swift 6.4
func value(for key: String) -> Int? {
    queue.sync { values[key] }
}
func set(_ value: Int, for key: String) {
    queue.async(flags: .barrier) { self.values[key] = value }
}

Common mistakes

  • Reading with async and returning a variable before the closure runs.
  • Using a barrier on a global queue (it acts like a normal async).
  • Assuming reads and writes are both safe on a plain concurrent queue.

Senior take & follow-ups

Seniors can write the barrier correctly from memory and still recommend an actor in new code.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why not use a serial queue?

    Show hint

    It serializes reads too; the barrier allows parallel reads.

  2. How would you port this to Swift 6?

    Show hint

    Use an actor, or a Mutex if callers must be synchronous.

Suggest a fix if something here is wrong or out of date.