问题是你的ReaderWriterLock
。您正在保存writeClosure
作为属性,然后异步分派调用该保存的属性的闭包。但如果另一个exclusiveWrite
在此期间,您的writeClosure
财产将被新的关闭所取代。
在这种情况下,这意味着您可以添加相同的Person
多次。由于您使用的是字典,因此这些重复项具有相同的键,因此不会导致您看到所有 1000 个条目。
你实际上可以简化ReaderWriterLock
,完全消除该属性。我也会做concurrentRead
一个泛型,返回值(就像sync
),并重新抛出任何错误(如果有)。
public class ReaderWriterLock {
private let queue = DispatchQueue(label: "com.domain.app.rwLock", attributes: .concurrent)
public func concurrentlyRead<T>(_ block: (() throws -> T)) rethrows -> T {
return try queue.sync {
try block()
}
}
public func exclusivelyWrite(_ block: @escaping (() -> Void)) {
queue.async(flags: .barrier) {
block()
}
}
}
其他一些不相关的观察结果:
-
顺便说一句,这简化了ReaderWriterLock
恰好解决了另一个问题。那writeClosure
我们现在已经删除的属性可以很容易地引入强引用循环。
是的,您在使用时非常谨慎[weak self]
,所以没有任何强引用循环,但这是可能的。我建议无论您在何处使用闭包属性,都将该闭包属性设置为nil
当您完成它时,因此闭包可能意外引起的任何强引用都将得到解决。这样持久的强引用循环就永远不可能。 (另外,闭包本身以及它所具有的任何局部变量或其他外部引用都将被解析。)
-
你睡了 10 秒。这应该足够了,但我建议不要只添加随机数sleep
调用(因为你永远无法 100% 确定)。幸运的是,您有一个并发队列,因此您可以使用它:
concurrentTestQueue.async(flags: .barrier) {
print(phoneBook.count)
}
由于存在这个障碍,它将等到您放入该队列的其他所有内容都完成为止。
-
注意,我不只是打印nameToPersonMap.count
。该数组已在内部仔细同步PhoneBook
,所以你不能让随机的外部类直接访问它而不同步。
每当您有一些要在内部同步的属性时,它应该是private
然后创建一个线程安全函数/变量来检索您需要的任何内容:
public class PhoneBook {
private var nameToPersonMap = [String: Person]()
private var phoneNumberToPersonMap = [String: Person]()
...
var count: Int {
return readWriteLock.concurrentlyRead {
nameToPersonMap.count
}
}
}
-
你说你正在测试线程安全,但随后创建了PhoneBook
with .none
选项(不实现线程安全)。在那种情况下,我预计会出现问题。你必须创建你的PhoneBook
与.threadSafe
option.
-
你有多个strongSelf
模式。那就比较不快了。在 Swift 中通常不需要它,因为您可以使用[weak self]
然后只进行可选链接。
将所有这些放在一起,这是我的最后一个游乐场:
PlaygroundPage.current.needsIndefiniteExecution = true
public class Person {
public let name: String
public let phoneNumber: String
public init(name: String, phoneNumber: String) {
self.name = name
self.phoneNumber = phoneNumber
}
public static func uniquePerson() -> Person {
let randomID = UUID().uuidString
return Person(name: randomID, phoneNumber: randomID)
}
}
extension Person: CustomStringConvertible {
public var description: String {
return "Person: \(name), \(phoneNumber)"
}
}
public enum ThreadSafety { // Changed the name from Qos, because this has nothing to do with quality of service, but is just a question of thread safety
case threadSafe, none
}
public class PhoneBook {
private var threadSafety: ThreadSafety
private var nameToPersonMap = [String: Person]() // if you're synchronizing these, you really shouldn't expose them to the public
private var phoneNumberToPersonMap = [String: Person]() // if you're synchronizing these, you really shouldn't expose them to the public
private var readWriteLock = ReaderWriterLock()
public init(_ threadSafety: ThreadSafety) {
self.threadSafety = threadSafety
}
public func personByName(_ name: String) -> Person? {
if threadSafety == .threadSafe {
return readWriteLock.concurrentlyRead { [weak self] in
self?.nameToPersonMap[name]
}
} else {
return nameToPersonMap[name]
}
}
public func personByPhoneNumber(_ phoneNumber: String) -> Person? {
if threadSafety == .threadSafe {
return readWriteLock.concurrentlyRead { [weak self] in
self?.phoneNumberToPersonMap[phoneNumber]
}
} else {
return phoneNumberToPersonMap[phoneNumber]
}
}
public func addPerson(_ person: Person) {
if threadSafety == .threadSafe {
readWriteLock.exclusivelyWrite { [weak self] in
self?.nameToPersonMap[person.name] = person
self?.phoneNumberToPersonMap[person.phoneNumber] = person
}
} else {
nameToPersonMap[person.name] = person
phoneNumberToPersonMap[person.phoneNumber] = person
}
}
var count: Int {
return readWriteLock.concurrentlyRead {
nameToPersonMap.count
}
}
}
// A ReaderWriterLock implemented using GCD concurrent queue and barriers.
public class ReaderWriterLock {
private let queue = DispatchQueue(label: "com.domain.app.rwLock", attributes: .concurrent)
public func concurrentlyRead<T>(_ block: (() throws -> T)) rethrows -> T {
return try queue.sync {
try block()
}
}
public func exclusivelyWrite(_ block: @escaping (() -> Void)) {
queue.async(flags: .barrier) {
block()
}
}
}
for _ in 0 ..< 5 {
let iterations = 1000
let phoneBook = PhoneBook(.threadSafe)
let concurrentTestQueue = DispatchQueue(label: "com.PhoneBookTest.Queue", attributes: .concurrent)
for _ in 0..<iterations {
let person = Person.uniquePerson()
concurrentTestQueue.async {
phoneBook.addPerson(person)
}
}
concurrentTestQueue.async(flags: .barrier) {
print(phoneBook.count)
}
}
就我个人而言,我倾向于更进一步
- 将同步移至通用类中;和
- change the model to be an array of
Person
object, so that:
- 该模式支持多人使用相同或电话号码;和
- 如果需要,您可以使用值类型。
例如:
public struct Person {
public let name: String
public let phoneNumber: String
public static func uniquePerson() -> Person {
return Person(name: UUID().uuidString, phoneNumber: UUID().uuidString)
}
}
public struct PhoneBook {
private var synchronizedPeople = Synchronized([Person]())
public func people(name: String? = nil, phone: String? = nil) -> [Person]? {
return synchronizedPeople.value.filter {
(name == nil || $0.name == name) && (phone == nil || $0.phoneNumber == phone)
}
}
public func append(_ person: Person) {
synchronizedPeople.writer { people in
people.append(person)
}
}
public var count: Int {
return synchronizedPeople.reader { $0.count }
}
}
/// A structure to provide thread-safe access to some underlying object using reader-writer pattern.
public class Synchronized<T> {
/// Private value. Use `public` `value` computed property (or `reader` and `writer` methods)
/// for safe, thread-safe access to this underlying value.
private var _value: T
/// Private reader-write synchronization queue
private let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronized", qos: .default, attributes: .concurrent)
/// Create `Synchronized` object
///
/// - Parameter value: The initial value to be synchronized.
public init(_ value: T) {
_value = value
}
/// A threadsafe variable to set and get the underlying object, as a convenience when higher level synchronization is not needed
public var value: T {
get { reader { $0 } }
set { writer { $0 = newValue } }
}
/// A "reader" method to allow thread-safe, read-only concurrent access to the underlying object.
///
/// - Warning: If the underlying object is a reference type, you are responsible for making sure you
/// do not mutating anything. If you stick with value types (`struct` or primitive types),
/// this will be enforced for you.
public func reader<U>(_ block: (T) throws -> U) rethrows -> U {
return try queue.sync { try block(_value) }
}
/// A "writer" method to allow thread-safe write with barrier to the underlying object
func writer(_ block: @escaping (inout T) -> Void) {
queue.async(flags: .barrier) {
block(&self._value)
}
}
}