繁体   English   中英

同时写入和读取哈希表

[英]Writing to and reading from a hashtable simultaneously

似乎我应该能够实现一个向量类型的 object ,我可以像这样同时插入和读取它:

  1. 如果向量中有空间,我可以插入东西; 这不应该干扰阅读。
  2. 如果我必须重新分配,我可以分配,然后复制,然后更新指向数据的指针,然后释放。
  3. 如果我想从向量中读取,我只需要确保获取指向数据的指针并从中读取是原子完成的。

这样,如果我在重新分配时从向量读取,我只是从旧位置读取,这仍然有效。 (当然删除不会是线程安全的,但这很好;如果你想删除东西,你只需要考虑到这一点,这至少不会比你现在使用的std::vector差。 )

反过来,我应该能够毫不费力地将其调整为哈希表——只需将这些向量之一用于存储桶,并使用其中一个向量支持每个存储桶。 (我意识到您应该使用某种自平衡二叉树来支持存储桶以获得最佳的渐近复杂性,但是向量对于我的应用程序来说很好,我不想在这里偏离轨道。)

两个问题:

  1. 这有意义还是我错过了什么? (我不相信我对线程安全的直觉。)
  2. 如果是这样,是否可以使用 C++ 标准库中的一些容器作为原语来构建这个或类似的东西,或者我唯一的做法是从头开始编写整个东西? (当然,我想我会在几个地方使用std::atomic ,但是有什么方法可以在这里使用std::vectorstd::unordered_map之类的东西吗?)

或者,有没有关于这个主题的书或我可以阅读的东西?

编写线程安全代码的问题在于,很难涵盖代码并发运行时可能发生的所有情况。 最有问题的是,本土的线程安全数据结构可能看起来像预期的那样工作,但在生产中经常随机失败。

比基于锁的算法更复杂的是无锁或无等待算法。 无锁算法保证即使一个线程被挂起,其他线程也能取得进展。 无等待算法(无锁)保证所有线程都能取得进展。

除了实际的算法之外,您还必须始终考虑实现算法的平台。 多线程代码取决于编译器和处理器的 memory model,尤其是在不使用锁的情况下。 std::atomic提供对无锁/无等待算法所需的原子原语的平台无关访问。 但是,这并没有使编写正确的自定义线程安全数据结构变得容易得多。

简短的回答是:不要这样做。

长答案:

最重要的一点是您需要数据结构的确切场景。 在此基础上,您可以得出需求并评估自己实施是否可行。 为了理解这种实现的底层机制,实验是有意义的。 为了生产代码,这通常会再次困扰您,因此很少会获胜。

由于您不能依赖标准容器的未定义行为(接口契约不能暗示的行为),因此很难将它们用作实现的基础。 文档通常定义单线程 POV 的预期行为。 但是,对于多线程,您需要了解内部结构才能依赖它们——当然,除非数据结构是在考虑到并发性的情况下实现的。

回到你的场景:假设你需要一个 hash 表,它有固定数量的桶,可以在没有阻塞的情况下读取。 插入可以序列化,不需要删除。 这对于缓存来说很常见。

作为构建块,您只需要一个锁和固定数量的链表,这些链表代表 hash 表存储桶并处理冲突。

查找算法如下(伪代码):

node* lookup(key) {
  // concurrency issue (see below)
  node = buckets[hash(key)].find(key);
  if (node) {
    return node;
  }
  lock();
  node = buckets[hash(key)].find(key);
  if (node) {
    return node;
  }
  node = new node(key);
  // concurrency issue (see below)
  buckets[hash(key)].add(node);
  unlock();
  return node;
}

hash 表可以无阻塞读取,插入是序列化的。 这仅适用于从未从存储桶中删除项目的情况。 否则,您可能会访问已释放的数据。

还有第二个警告不是很明显,它说明了编写多线程代码的复杂性。 仅当新创建的节点在其指针插入存储桶之前已完全分配并且对其他线程可见时,这才能按预期工作。 如果不维护该顺序,则读取器可能会触发分段错误,因为它们访问了部分初始化的节点。 顺序受编译器和 CPU 的影响,只要单线程代码的 POV 行为不改变,它们都可以自由地重新排序指令。

在这种特定情况下,订单是高度相关的。 因此,我们需要通知编译器和 CPU, new必须在add之前发生。 此外,读取器 ( find ) 需要在读取任何其他数据之前读取指针。 这是通过影响两个操作的 memory 顺序来实现的。 在 C++11 中,将节点指针表示为std::atomic<node*>并使用loadstore来读取/写入指针解决了该问题,因为默认的 memory 顺序是std::memory_order_seq_cst ,它提供了顺序一致性保证。 有一种更细致入微的方法可能会生成更高效的代码(使用std::memory_order_acquire进行load ,使用std::memory_order_release进行store )。 您还可以通过适当放置所谓的 memory 障碍/栅栏来影响订单(这些是由提到的 memory 订单 arguments 隐式触发的)。

纯粹基于锁的算法通常不必处理 memory 排序的原因是锁定原语已经在每个lockunlock隐式触发 memory 屏障/栅栏。

长话短说:如果您不需要创建自己的线程安全数据结构,请不要这样做,而是依赖已经过彻底审查和测试的现有实现。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM