如何在 Swift 中处理竞争条件读/写问题?

sal*_*qui 4 concurrency semaphore grand-central-dispatch ios swift

  1. 我从Raywenderlich帖子示例中得到了一个带有调度屏障的并发队列

    private let concurrentPhotoQueue = DispatchQueue(label: "com.raywenderlich.GooglyPuff.photoQueue", attributes: .concurrent)

写操作在哪里完成

func addPhoto(_ photo: Photo) {
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    // 1
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

虽然读取操作是在

var photos: [Photo] {
  var photosCopy: [Photo]!

  // 1
  concurrentPhotoQueue.sync {

    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}
Run Code Online (Sandbox Code Playgroud)

因为这将解决竞争条件。这里为什么只有Write操作是通过barrierRead in Sync 完成的。为什么 Read 不是用屏障完成,而是用同步写?。与同步写入一样,它会像锁一样等待直到读取,而屏障读取只会进行读取操作。

设置(10,forKey:“数字”)

打印(对象(forKey:“数字”))

设置(20,forKey:“数字”)

打印(对象(forKey:“数字”))

public func set(_ value: Any?, forKey key: String) {
        concurrentQueue.sync {
            self.dictionary[key] = value
        }
    }
    
    public func object(forKey key: String) -> Any? {
        // returns after concurrentQueue is finished operation
        // beacuse concurrentQueue is run synchronously
        var result: Any?
        
        concurrentQueue.async(flags: .barrier) {
            result = self.dictionary[key]
        }
        
        return result
    }
Run Code Online (Sandbox Code Playgroud)

使用翻转行为,我两次都为零,写入障碍给出 10 和 20 正确

Rob*_*Rob 7

你问:

为什么 Read 没有用屏障完成...?。

在这种读写器模式,您不使用屏障“读”操作,因为读取允许相对于其他同时发生的“读”,不影响线程安全。这是读写模式背后的全部动机,允许并发读取。

因此,您可以对“读取”使用屏障(它仍然是线程安全的),但如果碰巧同时调用多个“读取”请求,则会对性能产生不必要的负面影响。如果两个“读”操作可以同时发生,为什么不让它们发生呢?除非绝对需要,否则不要使用障碍(降低性能)。

最重要的是,只有“写入”需要与屏障一起发生(确保它们不会与任何“读取”或“写入”同时进行)。但是“读取”不需要(或不希望)任何障碍。

[为什么不] ...同步写入?

可以用 来“写” sync,但是,你为什么要这样写?它只会降低性能。假设您有一些尚未完成的读取,并且您使用屏障分派了“写入”。调度队列将为我们确保使用屏障调度的“写入”不会与任何其他“读取”或“写入”同时发生,那么为什么调度该“写入”的代码应该坐在那里等待“写”要完成吗?

使用syncfor write 只会对性能产生负面影响,而不会带来任何好处。问题不是“为什么不写sync?” 而是“为什么你会用写的sync?” 后一个问题的答案是,您不想不必要地等待。当然,您必须等待“读取”,而不是“写入”。

你提到:

随着翻转行为,我越来越nil......

是的,让我们考虑一下您假设的“读取”操作async

public func object(forKey key: String) -> Any? {
    var result: Any?

    concurrentQueue.async {
        result = self.dictionary[key]
    }

    return result
}
Run Code Online (Sandbox Code Playgroud)

这个有效的意思是“设置一个名为 的变量result,分派任务以异步检索它但不要等待读取完成后再返回result当前包含的任何内容(即,nil)。”

您可以看到为什么读取必须同步发生,因为您显然无法在更新变量之前返回值!


因此,重新编写后一个示例,您可以无障碍地同步读取,但使用障碍异步写入:

public func object(forKey key: String) -> Any? {
    return concurrentQueue.sync {
        self.dictionary[key]
    }
}

public func set(_ value: Any?, forKey key: String) {
    concurrentQueue.async(flags: .barrier) {
        self.dictionary[key] = value
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,因为sync“读取”操作中的方法将返回闭包返回的任何内容,因此您可以大大简化代码,如上所示。

或者,就我个人而言,我只是编写自己的下标运算符,而不是object(forKey:)and :set(_:forKey:)

public subscript(key: String) -> Any? {
    get {
        concurrentQueue.sync { 
            dictionary[key] 
        } 
    }

    set { 
        concurrentQueue.async(flags: .barrier) {
            self.dictionary[key] = newValue
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以做这样的事情:

store["Number"] = 10
print(store["Number"])
store["Number"] = 20
print(store["Number"])
Run Code Online (Sandbox Code Playgroud)

请注意,如果您发现这种读写器模式过于复杂,请注意您可以只使用串行队列(这就像对“读取”和“写入”使用屏障一样)。您仍然可能会sync“读取”和async“写入”。这也有效。但是在“读取”争用较多的环境中,它的效率仅比上述读写器模式低一点。