在 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 小心不要修改它已经插入到 map
的 BigStruct_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_free
和 is_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
我有一个没有任何方法的大型普通结构。它包括许多字段和另一个容器(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 小心不要修改它已经插入到 map
的 BigStruct_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_free
和 is_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