【问题标题】:Multithreaded read-many, write-seldom array/vector iteration in C++C ++中的多线程读取多,写入很少的数组/向量迭代
【发布时间】:2026-01-26 21:30:01
【问题描述】:

我需要以只读方式几乎不断地迭代一系列结构,但对于每 1M+ 次读取,其中一个线程可能会附加一个项目。我认为在这里使用互斥锁会有点矫枉过正,我还在某处读到读/写锁对读者有其自身的缺点。

我正在考虑在 std::vector 上使用 reserve() 但这个答案 Iterate over STL container using indices safe way to avoid using locks? 似乎无效。

关于哪种方式可能最快的任何想法?最重要的是让读者能够在尽可能少的争用情况下快速有效地进行迭代。写入操作对时间不敏感。

更新:我的另一个用例是“列表”可以包含指针而不是结构。即,std::vector。同样的要求也适用。

更新 2:假设示例

全球可访问:

typedef std::vector<MyClass*> Vector;
Vector v;
v.reserve(50);

阅读器线程 1-10:(这些线程几乎一直在运行)

.
.
int total = 0;
for (Vector::const_iterator it = v.begin(); it != v.end(); ++it)
{
   MyClass* ptr = *it;
   total += ptr->getTotal();
}
// do something with total
.
.

编写器线程 11-15:

MyClass* ptr = new MyClass();
v.push_back(ptr);

这基本上就是这里发生的事情。线程 1-15 都可以同时运行,尽管通常只有 1-2 个读取线程和 1-2 个写入线程。

【问题讨论】:

  • 这些向量有多大?它们必须是数组/向量吗?您需要内存块连续性吗?您可以将其更改为即元素列表或 1024 元素块列表等吗?
  • 并发结构的数量是否有界?它们是否曾经被删除或原地变异,或者您真的只是一遍又一遍地读取相同的值?有多少读者?
  • @quetzalcoatl 向量的范围从大约 2-20 个项目,平均大约 7 个。它们不需要内存连续性,但我认为具有这种连续性可能更可取 w.r.t 缓存位置。我愿意接受任何建议。
  • 好的,如果上面的大小没有限制,你就不能保留一个向量并且保证它永远不需要重新分配。所以,你需要一些可以成长的东西。多个“读者”可以同时尝试增加列表吗?
  • 一种可能的方式:数据有一个读者副本和一个作者副本;读者阅读他们的副本,作者更新他们的副本(带锁);偶尔(比如每 10k 读者的迭代),读者的副本会从作者的副本中更新。读者将等到这完成。缺点是所有读者都必须等到最后一个读者完成其周期。如果读取器之间不需要始终保持一致性,那么每个读取器可以独立切换,但您将需要更多的数据副本。

标签: c++ performance stl locking


【解决方案1】:

我认为可以在这里工作的是 vector 的自己的实现,如下所示:

template <typename T> class Vector
{
// constructor will be needed of course
public:
    std::shared_ptr<const std::vector<T> > getVector()
        { return mVector; }
    void push_back(const T&);

private:
    std::shared_ptr<std::vector<T> > mVector;
};

然后,当读者需要访问特定的Vector 时,他们应该调用getVector()保留返回的shared_ptr 直到完成阅读。

但作者应该始终使用Vectorpush_back 来增加新价值。然后这个push_back 应该检查mVector.size() == mVector.capacity() 是否为真,如果为真,分配new vector 并将其分配给mVector。比如:

template <typename T> Vector<T>::push_back(const T& t)
{
    if (mVector->size() == mVector->capacity())
    {
        // make certain here that new_size > old_size
        std::vector<T> vec = new std::vector<T> (mVector->size() * SIZE_MULTIPLIER);

        std::copy(mVector->begin(), mVector->end(), vec->begin());
        mVector.reset(vec);
    }
// put 't' into 'mVector'. 'mVector' is guaranteed not to reallocate now.
}

这里的想法受到 RCU(读取-复制-更新)算法的启发。如果存储空间用尽,只要至少有一个读取器访问新存储,就不应使旧存储失效。但是,应该分配新的存储,并且分配后的任何读者都应该能够看到它。一旦没有人再使用旧存储,就应该释放它(所有读取器都已完成)。

由于大多数硬件架构都提供了一些原子增量和减量的方法,我认为shared_ptr(因此Vector)将能够完全无锁地运行。

不过,这种方法的一个缺点是,根据读者持有 shared_ptr 的时间长短,您最终可能会收到多个 data 的副本。

PS:希望我没有在代码中犯太多尴尬的错误:-)

【讨论】:

  • 这真的是完全无锁吗?如果是,那么它绝对适合我的需求。写入可能非常昂贵,但读取需要快速且争用最少。
  • 我想知道这是否可行,实际上 push_back() 方法中不一定需要锁定。
  • @stgtscc:我很确定它是无锁的当且仅当shared_ptr 使用特定于硬件的原子增量和减量。有关这方面的详细信息,您需要了解您的编译器/库为您的特定硬件做了什么。
  • @stgtscc:我建议您使用您的编译器/硬件验证这一点。 Intel 上的 GCC 4.6.3 产生 lock add %edx,(%rax) 来增加引用计数,所以,我认为它应该能够在其他支持原子增量(或加载/存储)的平台上使用无锁增量。
【解决方案2】:

... 在 std::vector 上使用 reserve() ...

只有在你能保证向量永远不需要增长的情况下才有用。您已声明如果项目的数量没有在上面,因此您无法提供该保证。

尽管有链接的问题,但您可以想象使用 std::vector 只是为您管理内存,但要解决已接受答案中确定的问题,需要额外的逻辑层。


实际答案是:最快的做法是尽量减少同步量。同步的最小量是多少取决于你没有指定的代码和使用细节。


例如,我使用固定大小块的链表绘制了一个解决方案。这意味着您的常见用例应该与数组遍历一样高效,但您可以动态增长而无需重新分配。

然而,实现对以下问题很敏感:

  • 是否需要删除项目
    • 无论何时阅读?
    • 只能从前面还是从其他地方?
  • 如果容器是空的,是否要让阅读器忙-等待
    • 这是否应该使用某种退避方式
  • 需要什么程度的一致性?

【讨论】:

  • 如果有帮助,我添加了一个代码示例。虽然我认为上面的 shared_ptr 解决方案可能符合要求。我希望尽量减少同步。