如何处理 Swift 中的 Race Condition Read/Write 问题?

How to handle Race Condition Read/Write Problem in Swift?

  1. 我从 Raywenderlich post Example[=16 得到了一个带有调度屏障的并发队列=]

    private let concurrentPhotoQueue = DispatchQueue(标签:“com.raywenderlich.GooglyPuff.photoQueue”,属性:.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()
    }
  }
}

读操作已在

中完成
var photos: [Photo] {
  var photosCopy: [Photo]!

  // 1
  concurrentPhotoQueue.sync {

    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

因为这将解决 竞争条件。这里为什么只有 Write 操作在 barrierRead 中完成 Sync。为什么 Read 没有完成 barrier 和 write with sync ?和Sync Write一样,它会像锁一样等到它读取,而barrier Read它只会是读操作。

set(10, forKey: "Number")

print(object(forKey: "Number"))

set(20, forKey: "Number")

print(object(forKey: "Number"))

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
    }

通过翻转行为,我两次都得到 nil,Write 上有障碍,它给出 10 和 20 正确

你问:

Why is Read not done with barrier ... ?.

在这个 reader-writer 模式中,您不对“读取”操作使用屏障,因为读取 允许与其他“读取”同时发生”,而不影响线程安全。这是 reader-writer 模式背后的全部激励思想,允许并发读取。

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

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

[Why not] ... write with sync?

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

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

你提到:

With the flip behavior, I am getting nil ...

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

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

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

    return result
}

这有效地说“设置一个名为 result 的变量,分派任务以异步方式 检索它, 但不要在 [= 之前​​等待读取完成80=]ing 当前包含的任何 result(即 nil)。”

你可以明白为什么读取必须同步发生,因为你显然不能在更新变量之前return一个值!


因此,修改后一个示例,您可以无障碍同步读取,但有障碍异步写入:

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
    }
}

注意,因为“读取”操作中的sync方法将return无论闭包returns,你可以大大简化代码,如上所示。

或者,就个人而言,而不是 object(forKey:)set(_:forKey:),我只是写我自己的 subscript operator:

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

    set { 
        concurrentQueue.async(flags: .barrier) {
            self.dictionary[key] = newValue
        }
    }
}

然后您可以执行以下操作:

store["Number"] = 10
print(store["Number"])
store["Number"] = 20
print(store["Number"])

注意,如果你觉得这个 reader-writer 模式太复杂,请注意你可以只使用串行队列(这就像为“读”和“写”使用屏障)。您仍然可能会 sync“读取”和 async“写入”。那也行。但在“读取”竞争激烈的环境中,它的效率仅比上述 reader-writer 模式低一点。