1

似乎我应该能够实现一个可以同时插入和读取的向量类型对象,如下所示:

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

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

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

两个问题:

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

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

4

1 回答 1

2

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

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

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

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

长答案:

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

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

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

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

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

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;
}

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

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

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

纯粹基于锁的算法通常不必处理内存排序的原因是锁定原语已经隐式地触发了内存屏障/lock栅栏unlock

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

于 2020-05-11T21:16:32.413 回答