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

2024-03-27

我正在尝试实现一个线程安全的 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 个并发线程,这些线程只需将一个新人员添加到电话簿中即可。每个人都是唯一的,因此在 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

谁能帮我弄清楚发生了什么事吗?我的锁定代码是否有问题,或者更糟糕的是我的测试的构建方式是否有问题?我对这个多线程问题空间非常陌生,谢谢!


问题是你的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()
        }
    }
}

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

  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 with .none选项(不实现线程安全)。在那种情况下,我预计会出现问题。你必须创建你的PhoneBook.threadSafe option.

  5. 你有多个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)
        }
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 从脚本内更改自动热键托盘图标

    如何从 Autohotkey 脚本中将托盘图标更改为 my ico 例如 当脚本暂停时 为此 我在托盘菜单中提出了自己的 暂停脚本 菜单项 SingleInstance ignore Menu Tray Tip AutoCase 0 11
  • IE9-11 检测变换样式:preserve-3d

    我为一个项目制作了一个 3d 类型的菜单 自然 IE 会引起问题 因为 IE10 即使 3d 变换工作 也不支持变换样式 preserve 3d 我尝试了解决方法 通过对 3d 菜单容器的每个子元素应用变换 但至少可以说 动画看起来很糟糕
  • 文本溢出:省略号显示不同的字符

    我这里遇到了一些 CSS 问题 看这张图片 https www flickr com photos 125543025 N07 saved 1 在此图像中 我为文本 INTENSE TRAINING 添加了 CSS 样式 sample st
  • 我们什么时候应该在 Django 中使用“db_index=True”?

    当我们应该定义db index True在模型字段上 我正在尝试优化应用程序并且我想了解更多信息db index 什么情况下我们应该使用它 文档说使用db index True在模型字段上用于加速查找 但在存储和内存方面略有缺点 我们应该使
  • 使用 QtWebEngine 将 C++ 对象暴露给 Qt 中的 Javascript

    使用 QtWebkit 可以通过以下方式将 C 对象公开给 JavascriptQWebFrame addToJavaScriptWindowObject如中所述https stackoverflow com a 20685002 5959
  • Android Espresso 单击按钮时出现错误

    我正在尝试使用 espresso 框架为 Android 应用程序编写一些 UI 测试 现在我只是检查启动屏幕上是否存在所有元素 然后尝试单击登录按钮 单击按钮时 测试由于错误而失败 我似乎无法理解为什么会发生这种情况 我的测试代码是 Ru
  • Qt 布局,在小部件大小更改后调整到最小大小

    基本上我有一个QGridLayout里面有一些小部件 最重要的是 2 个标签 我用它们将图像绘制到屏幕上 好吧 如果用户愿意 他可以更改传入图像的分辨率 从而强制标签调整大小 我们假设标签的初始大小是320x240 用户将 VideoMod
  • bash:gitolite:找不到命令

    我正在尝试使用 Gitolite 在 Gitlab 中创建一个新分支 我完成安装步骤 当我遇到 设置 gitolite 部分时 我遇到了麻烦 我跟着这个link http sitaramc github com gitolite setup
  • ASP.NET Core MVC 视图组件搜索路径

    在此处的文档中 https learn microsoft com en us aspnet core mvc views view components view aspnetcore 2 2 https learn microsoft
  • SimpleIoC - 在缓存中找不到类型:Windows.UI.Xaml.Controls.Frame

    第一次由 SimpleIoC 实例化我的 ViewModel 时 我遇到了以下错误 我相信我已经按应有的方式设置了容器 但由于某种原因 我仍然收到以下错误 任何想法或帮助将非常感激 Microsoft Practices ServiceLo
  • 将 Angular Web 组件 EventEmitter 监听到 javascript

    我在以下工具的帮助下创建了一个小型网络组件本文 https medium com IMM9O web components with angular d0205c9db08f使用角度元素 其中包括 Input and Output 我能够将
  • svn 强制迁移

    我正在考虑将我们的 svn 代码库迁移到 perforce 看看谷歌搜索结果 我确实找到了两个具有相同功能的工具 P4转换ftp ftp perforce com pub perforce tools p4convert docs inde
  • java'assert'和'if(){}else exit;'之间的区别

    java和java有什么区别assert and if else exit 我可以用吗if else exit代替assert 也许有点谷歌 您应该记住的主要事情是 if else 语句应该用于程序流程控制 而assert 关键字应该仅用于
  • R闪亮:使用闪亮的JS从数据表中获取信息

    我想读出所有列名称以及它们在数据表中显示的顺序 由于不同的原因 我无法使用 stateSave 等选项 我对 JS 没有什么把握 但我确信用它可以完成 所以我需要你帮助我 我尝试过类似的代码片段 datatable data callbac
  • React 错误:目标容器不是 DOM 元素

    我刚刚开始使用 React 所以这可能是一个非常简单的错误 但我们开始吧 我的html代码非常简单 load staticfiles
  • 从 JavaScript 中的 OnClientClick 事件中阻止 C# 中的 asp:Button OnClick 事件?

    我有一个asp Button在我的网页上 它调用 JavaScript 函数和代码隐藏方法 后者进行调用以导航到另一个页面 在 JavaScript 函数中 我正在检查条件 如果不满足这个条件 我想中止导航 以便OnClick方法未被调用
  • PLS-00103:遇到符号“;”当预期出现以下情况之一时:

    我正在尝试插入用户安全问题的答案 以用于密码重置功能 Ellucian 横幅 v8 提供了一个用于运行此 API 的 API 我对他们的 API 非常陌生 从下面的错误消息来看 我还远远没有正确运行它 任何帮助表示赞赏 我尝试在 Oracl
  • Keystore getEntry 在 Android 9 上返回 NULL

    c我已对存储在 Android 密钥库中的登录密码进行了加密和解密 在 Android 9 上 我观察到应用程序在尝试解密密码时崩溃 我无法重现它 但拥有 Pixel 3 的用户是崩溃的设备之一 下面是我如何从密钥库解密密码 private
  • Java 和/C++ 在多线程方面的差异

    我读过一些提示 多线程实现很大程度上取决于您正在使用的目标操作系统 操作系统最终提供了多线程能力 比如Linux有POSIX标准实现 而windows32有另一种方式 但我想知道编程语言水平的主要不同 C似乎为同步提供了更多选择 例如互斥锁
  • 如何使用 dql 从数据表中获取唯一值?

    我有一个表 其中有一列存储了各种值 我想使用 dql 从该表中检索唯一值 Doctrine Query create gt select rec school gt from Records rec gt where rec city ci

随机推荐

  • Maven编译器使用的JDK版本在哪里指定?

    当我没有在我的 pom xml 文件中定义如下内容时 在我的系统上的哪个位置为 Maven 定义了编译时使用哪个版本的 Java JDK 我的系统上安装了多个版本 JAVA HOME指向其中之一
  • 使用 SQL Server 2012 恢复多个数据库的最佳脚本?

    我必须恢复大约 60 个不同大小的 SQL 数据库 我在谷歌上搜索找到一个脚本来依次恢复所有数据库 只需从我的文件夹中一一挑选它 我没有那么成功 可能是因为我对 SQL 等还很陌生 它可以在 powershell 或 sql 命令行中完成
  • 无法使用处理程序解决 CalledFromWrongThreadException

    我会尽量保持简单 在我的主要活动中 我创建了一个处理程序 public class ARViewer extends ARDisplayActivity public final MHandler mHandler new MHandler
  • Linux 发行版二进制兼容性

    有没有办法在 Linux 发行版中制作二进制文件并在具有相同架构的另一个发行版上运行它 或者我应该在不同的发行版上编译和构建它 基于 Redhat Debian 的二进制文件发行版之间是否存在兼容性 我想在 fedora 上使用我的 Ubu
  • 尝试打开工作簿时的 VBA 错误处理

    我试图循环浏览文件夹中的所有文件 打开它们并删除文档信息 我在处理无法打开的文件或打开时出现有关禁用宏的弹出消息时遇到问题 我尝试使用错误恢复下一步和错误转到 0 来解决此问题 但随后我遇到运行时失败 因为当我尝试关闭已打开的文件时 我的工
  • 管理多个 AWS 账户凭证的推荐方法?

    通过以下方式管理多个 Amazon Web Services AWS 账户的最佳方式是什么boto 我熟悉博托配置 http docs pythonboto org en latest boto config tut html文件 我正在使
  • Eclipse 使用 jboss 进行远程调试

    如何在 JBOSS 服务器 4 x 版本的 Eclipse 中配置远程调试 So far Step1 我修改了run config文件 通过取消注释以下行 用于远程套接字调试的示例 JPDA 设置 JAVA OPTS JAVA OPTS X
  • 系统更新IMG的src。内存泄漏

    系统更新IMG的src 内存泄漏 我目前每 x 秒更新一张图像 我想到的几种方法如下 拿一个 var url ImageID attr src url 现在这个作品完美地改变了形象but导致内存泄漏 取二 所以它正在创建 DOM 元素 所以
  • 相当于 JMockIt 中的 times() 吗?

    我不认为 minInspiration 或 maxInspiration 等同于 Mockito 中的 times 有没有 请看这个问题 Mockito 和 JMockIt 之间的主要区别 https stackoverflow com q
  • 如何在 Artifactory 中触发 Jenkins 在新工件上构建?

    Artifactory 可以用作 Jenkins 中的 SCM 或源来触发特定工件部署上的构建吗 在 Artifactory Jenkins 插件中没有看到 或错过 任何类似的内容描述 https www jfrog com conflue
  • 选择 count(*) 并“连接”3 个表

    我正在寻找 3 个表的联接内的计数查询 它可以让我对其中一个表的不同值进行计数 我有 3 个表 需要连接它们才能获取预期数据 Workflow Message and Message Workflow Relation 我想获取按状态 已连
  • 为什么使用 Gradle 而不是 Ant 或 Maven? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • javaScript 函数 - 为什么我的默认参数失败?

    我的 Javascript 函数导致我的控制台返回我 类型错误 样式为空 这里是片段 let style one 1 two 2 three 3 function styling style style ruleSetStock retur
  • C# 中的OptionalField 已经过时了吗?

    看来可选字段属性 http msdn microsoft com en us library system runtime serialization optionalfieldattribute aspx不再需要 如果有的话 使用 Bin
  • MongoRepository @Query 无法将字符串解析为日期

    首先 我的问题是通过 Spring MongoDb 的 MongoRepository 在 MongoDB 中搜索集合 我的对象 id ObjectId 5c78e1f447f39c2eacb229d7 lab xxx type Holid
  • 在 Accordion menu.js 文件中放置图标而不是“+”或“-”

    我正在制作手风琴菜单 我刚刚找到这个链接http jsfiddle net zM5Vj http jsfiddle net zM5Vj 和我做的手风琴菜单几乎相似 代码中 有 if this text this text else acco
  • SugarCRM:如何通过 REST API 获取帐户的所有联系人

    我正在尝试使用 v2 REST API 从 SugarCRM 获取特定帐户 我知道帐户 ID 的所有联系人 我正在发送带有以下参数的 GET 请求 input type gt JSON response type gt JSON metho
  • -fsanitize 在 GCC-6.1 中不使用 gold 链接器

    更新 2016 年 9 月 30 日 Ubuntu 版本的 gcc 6 2 Ubuntu 6 2 0 3ubuntu11 16 04 6 2 0 20160901 不再有这个问题 我使用的是 Ubuntu 版本的 gcc 6 1 1 Ubu
  • 访问 TeamCity 构建评论

    继续回答这个问题 是否可以向团队城市构建添加自由文本注释 https stackoverflow com questions 4780955 is it possible to add a free text note to a team
  • 如何在 Swift 中实现线程安全哈希表(PhoneBook)数据结构?

    我正在尝试实现一个线程安全的 PhoneBook 对象 电话簿应该能够添加一个人 并根据姓名和电话号码查找一个人 从实现的角度来看 这仅涉及两个哈希表 一个关联名称 gt 人员 另一个关联电话 gt 人员 需要注意的是我希望这个对象是线程安