如何在Swift中处理种族条件读写?

时间:2018-08-30 12:52:10

标签: swift concurrency grand-central-dispatch

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

读入是在

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

  // 1
  concurrentPhotoQueue.sync {

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

因为这将解决比赛条件。在这里,为什么只对 Sync 中的 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
    }

在翻转行为下,我两次都为零,并且在写入时遇到障碍,它给出10和20的正确值

1 个答案:

答案 0 :(得分:1)

您问:

  

为什么不使用屏障...来完成读取??

在这种读写模式中,您不要对“读取”操作使用障碍,因为允许读取 与其他“读取”同时发生,而不影响线程安全。因此,您可以可以将屏障与“读取”一起使用(它仍然是线程安全的),但是如果碰巧同时调用多个“读取”请求,则会不必要地对性能产生负面影响。如果两个“读取”操作可以彼此同时发生,为什么不让它们呢?除非绝对必要,否则请不要使用障碍物(降低性能)。

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

  

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

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

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

您提到:

  

通过翻转行为,我得到nil ...

是的,因此,请考虑使用async进行的假设的“读取”操作:

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

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

    return result
}

这个有效的说法是“设置一个名为result的变量,调度任务以异步地获取它,,但不要等到读取完成后再返回任何result当前包含(即nil)。”

您会看到为什么必须同步进行读取的原因,因为显然在更新变量之前您无法返回值!


因此,重做您的后一个示例,您可以无障碍地同步阅读,而使用障碍可以异步编写:

public func set(_ value: Any?, forKey key: String) {
    concurrentQueue.async {
        self.dictionary[key] = value
    }
}

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

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


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