在 C++ 中保持大的普通结构原子的更好方法?

Better way in C++ to keep a big plain struct atomic?

我有一个没有任何方法的大型普通结构。它包括许多字段和另一个容器(std::vector)。 我需要让它成为原子的,以便让一个生产者线程和许多消费者线程可以同时访问它。 消费者不修改数据,他们只是读取数据。只有一个生产者adds/modifys(不删除)数据,因此它必须保留消费者随时可以看到的数据integrity/consistency。

我认为有两种解决方案可以解决它,但我不知道哪种更好。

方法一:

struct Single_t {
   size_t idx;
   double val;
}
using Details_t = std::vector<Single_t>;

struct BigStruct_1 {
   std::string      id;
   size_t           count;
   Details_t        details;
   std::atomic_bool data_ok { false };
};
std::map<std::string, BigStruct_1> all_structs1;

方法二:

// Single_t and Details_t are same as the ones in Method1.
struct BigStruct_2 {
   std::string id;
   size_t      count;
   Details_t   details;
};
std::map<std::string, std::atomic<BigStruct_2> > all_structs2;

到目前为止我更喜欢Method2,因为我不需要检查是否data_ok处处都是消费者代码。事实上,消费者持有一个指向单个 BigStructs_2 的指针,我希望可以随时访问数据(数据不会被删除)。 还好我的系统只有一个producer,所以producer可以简单的一步覆盖数据:std::atomic<BigStruct_2>::store( tmp_pod );,所以我不需要检查是否数据是ready/OK(应该总是OK)当消费者正在读取数据时,我也不需要在这种情况下使用std::atomic::compare_exchange_xxx,所以没有必要提供比较两个 BigStruct_2 对象之间的算法。我说得对吗?

如果我使用Method1,当我需要修改数据时,我必须编写如下代码:

auto& one_struct = all_structs1["some_id"];
one_struct.data_ok.store( false, memory_order_release );
// change the data....
one_struct.data_ok.store( true, memory_order_release );

并检查是否data_ok在消费者代码中无处不在...它丑陋且危险(容易忘记检查)。

任何 recomments/hints 将不胜感激!

我建议你让你的 BigStructs 有效地不可变,并像这样存储它们:

std::map<std::string, std::shared_ptr<BigStruct_2> > all_structs2;

为了进行更新,writer-thread 将分配一个新的 BigStruct_2(使用 std::make_shared 或其他),将其设置为等于要修改的对象,然后修改新 BigStruct_2,然后将新 shared_ptr<BigStruct_2> 插入 map.

然后,每当 reader 线程想要读取 BigStruct_2 时,它可以通过保存从 std::map 检索到的本地 std::shared_ptr<BigStruct_2> 来安全地读取。因为你的 writer-thread 小心不要修改它已经插入到 mapBigStruct_2,所以 reader-thread 不可能被BigStruct_2的内容在阅读过程中发生变化。

请注意,上面的 none 使得对 std::map 本身的访问是线程安全的,因此您仍然需要使用互斥锁同步所有这些。不过,这些操作应该非常快,所以我怀疑这会是个问题。

另一种更新主要读取的共享对象的解决方案是seqlock:

Seqlocks are an important synchronization mechanism and represent a significant improvement over conventional reader-writer locks in some contexts. They avoid the need to update a synchronization variable during a reader critical section, and hence improve performance by avoiding cache coherence misses on the lock object itself.

std::atomic<BigStruct_2> 将有效地使用锁:is_always_lock_freeis_lock_free 很可能是 false 在最近的实施中。

此外,您无论如何都不能在 std::atomic 中使用非平凡可复制的类型(即在您的结构中没有 std::string)。

一般来说,std::atomic 与任何大小的类型一起工作的能力是为了可移植性(以防硬件不支持特定大小的原子,但有些支持)。它不适用于永不锁定的情况。

data_ok 的方法实际上也是基于锁的。当 data_ok 为假时,您需要重试。

您最好只使用 std::mutex 来保护数据,或者,如果读取很普遍,请使用 std::shared_mutex 以允许并发读取。

如果您希望绝对减少锁定时间,我会建议第三种方法 - 您有一个项目映射,这些项目是包含指向当前值的原子指针的结构,而不是共享指针映射.

这样做的好处是你不必锁定地图来更新值;增删改查只需要锁定地图即可

锁定地图实际上是一个域锁,它会阻止所有线程,而大多数时候你只需要阻止任何读取感兴趣的地图条目的线程。

您有效地对数据执行“写时复制”以修改它。所以你有

struct Single_t {
   size_t idx;
   double val;
}
using Details_t = std::vector<Single_t>;

struct BigStruct_Container {
   std::string      id;       //maybe put this in the digest if immutable?
   size_t           count;
   Details_t        details;
};

struct BigStruct_Digest {
   std::atomic<BigStruct_Container*> current_value;
};
std::map<std::string, BigStruct_Digest> all_digests;

有了这个,要执行更新,您需要复制数据并生成一个新容器。然后您只需更新地图中的容器指针(摘要)。由于您没有更改地图中的值,地图保持不变,因此更新时无需锁定地图。


关于使用共享指针的警告;您需要使用互斥锁来保护内部的引用计数机制,因为它不是线程安全的。如果您使用域锁应该没问题,但这不是最佳解决方案。


请注意,鉴于地图不包含太多数据,您甚至可以使用地图执行写时复制,并拥有地图本身的原子指针......


这与《七周七个并发模型》一书中的“锁定单个成员,而不是集合”的思路一致 - https://www.amazon.co.uk/Seven-Concurrency-Models-Weeks-Programmers/dp/1937785653