Search code examples
swiftgrand-central-dispatch

Swift: Thread safe Singleton, why do we use sync for read?


While making a thread-safe Singleton, it is advised to use a sync for read and an async with a barrier for write operation.

My question is why do we use a sync for read? What might happen if we perform a read with async operation?

Here is an example of what is recommended:

func getUser(id: String) throws -> User {
  var user: User!
  try concurrentQueue.sync {
    user = try storage.getUser(id)
  }
  return user
}
func setUser(_ user: User, completion: (Result<()>) -> Void) {
  try concurrentQueue.async(flags: .barrier) {
    do {
      try storage.setUser(user)
      completion(.value(())
    } catch {
      completion(.error(error))
    }
  }
}

Solution

  • It's all in what you want. By changing get user to async, then you need to use a callback to wait for the value.

    
    func getUser(id: String, completion: @escaping (Result<User>) -> Void) -> Void {
        concurrentQueue.async {
            do {
                let user = try storage.getUser(id)
                completion(.value(user))
            } catch {
                completion(.error(error))
            }
        }
    }
    
    func setUser(_ user: User, completion: @escaping (Result<()>) -> Void) {
        concurrentQueue.async(flags: .barrier) {
            do {
                try storage.setUser(user)
                completion(.value(()))
            } catch {
                completion(.error(error))
            }
        }
    }
    

    That changes the API of get user, so now when calling get user, a callback will need to be used.

    Instead of somethings like this

    do {
        let user = try manager.getUser(id: "test")
        updateUI(user: user)
    } catch {
        handleError(error)
    }
    

    you will need something like this

    manager.getUser(id: "test") { [weak self] result in
        switch result {
        case .value(let user):  self?.updateUI(user: user)
        case .error(let error): self?.handleError(error)
        }
    }
    

    Assuming you have somethings like a view controller with a property named manager and methods updateUI() and handleError()