(我对实现的设计感兴趣,而不是一个可以完成所有工作的现成结构。)
假设我们有一个类HashTable(不是作为树实现的哈希映射,而是哈希表) 并说有八个主题。 假设读写比率约为100:1甚至更好1000:1。 情况A)只有一个线程是编写器,其他包括编写器可以从HashTable读取(它们可能只是遍历整个哈希表) 情况B)所有线程都相同,都可以读/写。
有人可以提出最佳策略,以便通过以下考虑使类线程安全 1.最小锁定争用的最高优先级 2.锁定次数最少的第二优先级
到目前为止,我的理解是: 一个BIG读写器锁(信号量)。 专门化信号量,以便案例B可以有8个实例编写器资源,其中每个编写器资源锁定一行(或该范围的范围)。 (所以我想1 + 8互斥)
如果我正在考虑正确的问题,请告诉我,我们如何才能改进此解决方案。
答案 0 :(得分:7)
如此高的读/写比率,您应该考虑无锁解决方案,例如nbds
编辑:
通常,无锁算法的工作方式如下:
在争用率非常低的情况下,这是对锁定算法的性能胜利,因为函数大多数是第一次成功,而不会产生获取锁的开销。随着争用的增加,收益变得更加可疑。
通常,可以原子操作的数据量很小 - 通常是32位或64位 - 因此对于涉及许多读取和写入的函数,生成的算法变得复杂并且可能很难推理。出于这个原因,最好为您的问题寻找和采用成熟的,经过良好测试和易于理解的第三方无锁解决方案,而不是自己推出。
Hashtable实现细节将取决于散列和表设计的各个方面。我们希望能够成长吗?如果是这样,我们需要一种方法将旧表中的批量数据安全地复制到新表中。我们是否期望哈希冲突?如果是这样,我们需要一些走路碰撞数据的方法。我们如何确保另一个线程不会删除返回它的查找与使用它的调用者之间的键/值对?也许某种形式的引用计数? - 但谁拥有参考? - 或者只是复制查找值? - 但是如果值很大会怎么样?
无锁堆栈很容易理解并且相对简单地实现(从堆栈中删除一个项目,获取当前顶部,尝试用它的下一个指针替换它,直到你成功,返回它;添加一个item,获取当前顶部并将其设置为项的下一个指针,直到您成功将指向该项的指针编写为新顶部;在具有保留/条件写入语义的体系结构上,这就足够了,仅支持您需要的CAS的体系结构将随机数或版本号附加到原子操纵的数据以避免ABA problem)。它们是以原子锁自由方式跟踪键/数据的可用空间的一种方法,允许您将键/值对(实际存储在哈希表条目中的数据)减少到指针/偏移量或两个,一个小的使用您的架构的原子指令操纵足够的数量。还有其他人。
然后读取成为查找条目的情况,根据请求的密钥检查kvp,执行任何操作以确保在我们返回它时保留有效值(获取副本/增加其引用计数),检查自从我们开始读取后,该条目未被修改,如果是,则返回该值,撤消任何引用计数更改,如果不是则重复读取。 写作将取决于我们在碰撞方面所做的工作;在简单的情况下,它们只是找到正确的空槽并编写新的kvp的情况。 以上内容大大简化,不足以产生您自己的安全实现,特别是如果您不熟悉无锁/无等待技术。可能的并发症包括ABA问题,优先级倒置,特定线程的饥饿;我没有解决哈希冲突问题。
nbds页面链接到允许增长/碰撞的真实世界方法的excellent presentation。其他人存在,快速谷歌找到了很多论文。
免费锁定等待免费算法是研究的迷人领域;我鼓励读者到谷歌周围。也就是说,天真无锁实现可以很容易看起来合理,并且在大多数情况下表现正确,而实际上是微妙的不安全。虽然掌握这些原则非常重要,但我强烈建议您使用现有的,易于理解且经过验证的实施,而不是自己动手实施。
答案 1 :(得分:2)
您可能希望查看Java的ConcurrentHashMap实现以了解一种可能的实现。
NOT 的基本思想是锁定每次读取操作,但仅限于写入。因为在你的采访中他们特别提到了一个非常高的读取:写入比率,尝试尽可能多的开销写入是有意义的。
ConcurrentHashMap将哈希表划分为所谓的“段”,这些段本身是可同时读取的哈希表,并使每个段保持一致状态,以允许遍历而不锁定。
在阅读时你基本上有通常的hashmap get()与你必须担心读取陈旧值的区别,所以像正确的节点的值,段表的第一个节点和下一个指针必须是volatile(使用c ++不存在的内存模型,你可能无法移植; c ++ 0x应该有帮助,但到目前为止还没看过它。)
当在那里放置一个新元素时,你将获得所有开销,首先必须锁定给定的段。锁定后它基本上是一个常见的put()操作,但是你必须保证在更新节点的下一个指针时指向原子写入(指向新创建的节点,其下一个指针必须已经正确指向旧的下一个节点)或覆盖节点的价值。
在扩展细分时,您必须重新散列现有节点并将它们放入新的更大的表中。重要的是克隆新表的节点,以便不影响旧表(通过过早更改它们的下一个指针),直到新表完成并替换旧表(他们在那里使用一些聪明的技巧,这意味着他们只有克隆大约1/6的节点 - 很好,但我不确定他们是如何达到这个数字的)。 请注意,垃圾收集使这一切变得更加容易,因为您不必担心未重复使用的旧节点 - 只要所有读取器完成,它们就会自动进行GC控制。这是可以解决的,但我不确定最好的方法是什么。
我希望基本的想法有点明确 - 显然有几点不是简单地移植到c ++,但它应该给你一个好主意。
答案 2 :(得分:1)
无需锁定整个表,只需每个桶锁一次。这立即给出了并行性。向表中插入新节点需要锁定存储桶即将修改头节点。总是在表头添加新节点,以便读者可以遍历节点而不必担心看到新节点。
每个节点都有一个r / w锁;读者迭代得到一个读锁定锁。节点修改需要写锁定。
没有桶锁导致节点删除的迭代需要尝试获取桶锁,如果失败则必须释放锁并重试以避免死锁,因为锁定顺序不同。
简要概述。
答案 3 :(得分:-1)
您可以尝试使用atomic_hashtable进行c https://github.com/Taymindis/atomic_hashtable用于读取,写入和删除,而不会在多线程访问数据时锁定,简单和稳定
README中给出的API文档。