如何在Swift中实现线程安全哈希表(PhoneBook)数据结构?

时间:2018-03-05 00:24:34

标签: swift multithreading grand-central-dispatch data-synchronization barrier

我正在尝试实现一个Thread-Safe PhoneBook对象。电话簿应该能够添加一个人,并根据他们的姓名和phoneNumber查找一个人。从实现的角度来看,这只涉及两个哈希表,一个关联名称 - >人和另一个关联电话# - >人。

警告我希望这个对象是threadSafe。这意味着我希望能够支持PhoneBook中的并发查找,同时确保一次只有一个线程可以将Person添加到PhoneBook中。这是读写器的基本问题,我正在尝试使用GrandCentralDispatch和调度障碍来解决这个问题。我正在努力解决这个问题,因为我遇到了问题。下面是我的Swift游乐场代码:

//: Playground - noun: a place where people can play

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

public class Person: CustomStringConvertible {
    public var description: String {
        get {
            return "Person: \(name), \(phoneNumber)"
        }
    }

    public var name: String
    public var phoneNumber: String
    private var readLock = ReaderWriterLock()

    public init(name: String, phoneNumber: String) {
        self.name = name
        self.phoneNumber = phoneNumber
    }


    public func uniquePerson() -> Person {
        let randomID = UUID().uuidString
        return Person(name: randomID, phoneNumber: randomID)
    }
}

public enum Qos {
    case threadSafe, none
}

public class PhoneBook {

    private var qualityOfService: Qos = .none
    public var nameToPersonMap = [String: Person]()
    public var phoneNumberToPersonMap = [String: Person]()
    private var readWriteLock = ReaderWriterLock()


    public init(_ qos: Qos) {
        self.qualityOfService = qos
    }

    public func personByName(_ name: String) -> Person? {
        var person: Person? = nil
        if qualityOfService == .threadSafe {
            readWriteLock.concurrentlyRead { [weak self] in
                guard let strongSelf = self else { return }
                person = strongSelf.nameToPersonMap[name]
            }
        } else {
            person = nameToPersonMap[name]
        }

        return person
    }

    public func personByPhoneNumber( _ phoneNumber: String) -> Person? {
        var person: Person? = nil
        if qualityOfService == .threadSafe {
            readWriteLock.concurrentlyRead { [weak self] in
                guard let strongSelf = self else { return }
                person = strongSelf.phoneNumberToPersonMap[phoneNumber]
            }
        } else {
            person = phoneNumberToPersonMap[phoneNumber]
        }

        return person
    }

    public func addPerson(_ person: Person) {
        if qualityOfService == .threadSafe {
            readWriteLock.exclusivelyWrite { [weak self] in
                guard let strongSelf = self else { return }
                strongSelf.nameToPersonMap[person.name] = person
                strongSelf.phoneNumberToPersonMap[person.phoneNumber] = person
            }
        } else {
            nameToPersonMap[person.name] = person
            phoneNumberToPersonMap[person.phoneNumber] = person
        }
    }

}


// A ReaderWriterLock implemented using GCD and OS Barriers.
public class ReaderWriterLock {

    private let concurrentQueue = DispatchQueue(label: "com.ReaderWriterLock.Queue", attributes: DispatchQueue.Attributes.concurrent)
    private var writeClosure: (() -> Void)!

    public func concurrentlyRead(_ readClosure: (() -> Void)) {
        concurrentQueue.sync {
            readClosure()
        }
    }

    public func exclusivelyWrite(_ writeClosure: @escaping (() -> Void)) {
        self.writeClosure = writeClosure
        concurrentQueue.async(flags: .barrier) { [weak self] in
            guard let strongSelf = self else { return }
            strongSelf.writeClosure()
        }
    }

}

// MARK: Testing the synchronization and thread-safety

for _ in 0..<5 {
    let iterations = 1000
    let phoneBook = PhoneBook(.none)

    let concurrentTestQueue = DispatchQueue(label: "com.PhoneBookTest.Queue", attributes: DispatchQueue.Attributes.concurrent)
    for _ in 0..<iterations {
        let person = Person(name: "", phoneNumber: "").uniquePerson()
        concurrentTestQueue.async {
            phoneBook.addPerson(person)
        }
    }

    sleep(10)
    print(phoneBook.nameToPersonMap.count)
}

要测试我的代码,我运行1000个并发线程,只需将新Person添加到PhoneBook中。每个Person都是唯一的,因此在1000个线程完成之后,我希望PhoneBook包含1000个计数。每次执行写操作时,我执行dispatch_barrier调用,更新哈希表,然后返回。据我所知,这就是我们需要做的一切;然而,在重复运行1000个线程后,我得到的电话簿中的条目数量不一致且遍布整个地方:

Phone Book Entries: 856
Phone Book Entries: 901
Phone Book Entries: 876
Phone Book Entries: 902
Phone Book Entries: 912

任何人都可以帮我弄清楚发生了什么事吗?我的锁定代码有什么问题,或者更糟糕的是我的测试结构如何?我对这个多线程问题空间很新,谢谢!

2 个答案:

答案 0 :(得分:5)

问题在于你ReaderWriterLock。您将writeClosure保存为属性,然后异步调度调用该已保存属性的闭包。但如果在此期间有另一个exclusiveWrite进入,则writeClosure属性将被新的闭包替换。

在这种情况下,这意味着您可以多次添加相同的Person。而且因为您使用的是字典,所以这些副本具有相同的密钥,因此不会导致您看到所有1000个条目。

您实际上可以简化ReaderWriterLock,完全消除该属性。我还将concurentRead设为通用,返回值(就像sync一样)。

public class ReaderWriterLock {

    private let concurrentQueue = DispatchQueue(label: "com.ReaderWriterLock.Queue", attributes: DispatchQueue.Attributes.concurrent)

    public func concurrentlyRead<T>(_ readClosure: (() -> T)) -> T {
        return concurrentQueue.sync {
            readClosure()
        }
    }

    public func exclusivelyWrite(_ writeClosure: @escaping (() -> Void)) {
        concurrentQueue.async(flags: .barrier) {
            writeClosure()
        }
    }

}

其他一些不相关的观察结果:

  1. 顺便说一下,这个简化的ReaderWriterLock碰巧解决了另一个问题。我们现在删除的writeClosure属性可能很容易引入强大的参考周期。

    是的,您对使用[weak self]一丝不苟,因此没有任何强大的参考周期,但这是可能的。我建议无论你在哪里使用一个闭包属性,你在完成它时都要将该闭包属性设置为nil,这样就可以解决闭包可能意外引起的任何强引用。这样,持久的强引用循环永远不可能实现。 (另外,关闭本身以及它所具有的任何局部变量或其他外部引用都将得到解决。)

  2. 你在睡觉10秒钟。这应该绰绰有余,但我建议不要只是添加随机sleep来电(因为你永远不能100%肯定)。幸运的是,你有一个并发队列,所以你可以使用它:

    concurrentTestQueue.async(flags: .barrier) { 
        print(phoneBook.count) 
    }
    

    由于这个障碍,它将等到你在该队列上放置的所有其他内容完成。

  3. 注意,我不只是打印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
            }
        }
    }
    
  4. 您说您正在测试线程安全性,但随后使用PhoneBook选项创建.none(实现无线程安全性)。在那种情况下,我会发现问题。您必须使用PhoneBook选项创建.threadSafe

  5. 您有多种strongSelf模式。那是相当不客气的。在Swift中通常不需要它,因为您可以使用[weak self]然后只进行可选链接。

  6. 将所有这些结合在一起,这是我最后的游乐场:

    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 concurrentQueue = DispatchQueue(label: "com.ReaderWriterLock.Queue", attributes: .concurrent)
    
        // if you make this generic, you can streamline some of the code that uses this
    
        public func concurrentlyRead<T>(_ readClosure: (() -> T)) -> T {
            return concurrentQueue.sync { readClosure() }
        }
    
        public func exclusivelyWrite(_ writeClosure: @escaping (() -> Void)) {
            concurrentQueue.async(flags: .barrier) { writeClosure() }
        }
    
    }
    
    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)
        }
    }
    

    就个人而言,我倾向于更进一步,

    • 将同步移动到泛型类中;和
    • 将模型更改为Person对象的数组,以便:
      • 该模型支持多人使用相同或电话号码;和
      • 如果需要,可以使用值类型。

    例如:

    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(value: [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
    
        public var value: T {
            get { return queue.sync { _value } }
            set { queue.async(flags: .barrier) { self._value = 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) -> U) -> U {
            return queue.sync { 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)
            }
        }
    }
    

答案 1 :(得分:-3)

我不认为你错了:)。

原始(在macos上)生成:

0  swift                    0x000000010c9c536a PrintStackTraceSignalHandler(void*) + 42
1  swift                    0x000000010c9c47a6 SignalHandler(int) + 662
2  libsystem_platform.dylib 0x00007fffbbdadb3a _sigtramp + 26
3  libsystem_platform.dylib 000000000000000000 _sigtramp + 1143284960
4  libswiftCore.dylib       0x0000000112696944 _T0SSwcp + 36
5  libswiftCore.dylib       0x000000011245fa92 _T0s24_VariantDictionaryBufferO018ensureUniqueNativeC0Sb11reallocated_Sb15capacityChangedtSiF + 1634
6  libswiftCore.dylib       0x0000000112461fd2 _T0s24_VariantDictionaryBufferO17nativeUpdateValueq_Sgq__x6forKeytF + 1074

如果从ReaderWriter队列中删除“.concurrent”,“问题就会消失”。© 如果还原.concurrent,但将编写器端的异步调用更改为同步:

swift(10504,0x70000896f000)malloc:***对象0x7fcaa440cee8的错误:释放对象的校验和不正确 - 对象可能在被释放后被修改。

如果它没有迅速,那会有点惊人吗? 我挖了进来,通过插入一个哈希函数替换了你的'string'数组,用一个哈希函数取代了sleep(10),用一个屏障调度替换了睡眠(10)以冲洗任何落后的块,这使得它更可重复地崩溃,更有帮助:

x(10534,0x700000f01000)malloc:***对象0x7f8c9ee00008错误:释放对象的校验和不正确 - 对象可能在被释放后被修改。

但是当搜索源没有发现malloc或free时,堆栈转储可能更有用。

无论如何,解决问题的最佳方法是:改用go;它实际上是有道理的。