C++ 在多个线程之间频繁传递值的最佳体系结构是什么?

C++ 在多个线程之间频繁传递值的最佳体系结构是什么?,c++,multithreading,concurrency,C++,Multithreading,Concurrency,我正在用C++14编写一个应用程序,它由一个主线程和多个从线程组成。主线程协调从属线程,从属线程协调执行搜索,每个线程探索搜索空间的一部分。从属线程有时会在搜索时遇到绑定。然后它将该绑定传递给主线程,主线程将该绑定发送给所有其他从线程,以便它们可以缩小搜索范围 从线程必须非常频繁地检查是否有新的绑定可用,可能在循环的入口 将绑定通信到从线程的最佳方式是什么?我可以考虑使用std::atomic,但我担心在循环中读取变量时会带来性能影响。基本上,您可以用您的体系结构打赌,对原始数据类型的一次写入就

我正在用C++14编写一个应用程序,它由一个主线程和多个从线程组成。主线程协调从属线程,从属线程协调执行搜索,每个线程探索搜索空间的一部分。从属线程有时会在搜索时遇到绑定。然后它将该绑定传递给主线程,主线程将该绑定发送给所有其他从线程,以便它们可以缩小搜索范围

从线程必须非常频繁地检查是否有新的绑定可用,可能在循环的入口


将绑定通信到从线程的最佳方式是什么?我可以考虑使用std::atomic,但我担心在循环中读取变量时会带来性能影响。

基本上,您可以用您的体系结构打赌,对原始数据类型的一次写入就是原子的。由于您只有一个writer,如果您使用volatile关键字来阻止编译器优化(可能只在本地缓存中对其执行更新),那么您的程序将不会中断

然而,每个人都认真对待做正确的事情,TM会告诉你其他的。看看这篇文章,可以得到一个非常好的风险评估:

所以如果你想站在安全的一边,我建议你遵循C++标准。由于C++标准不保证任何原子性,即使对于最简单的操作,也会使用STD::原子。但老实说,我不认为这太糟糕。当然有一个锁涉及,但你可以平衡阅读频率的好处是知道新的边界早


为了防止轮询原子变量,您可以使用POSIX信号机制通知从属线程更新,确保它与您正在编程的平台一起工作。如果这对性能有好处或没有好处,则需要观察。

这里最简单的方法是我不要过度考虑这一点。只需为每个线程使用std::mutex,即可保护边界信息所在的std::队列。让主线程等待每个子线程都可以锁定的std::condition_变量,写入一个新的边界队列,然后向te cv发送信号,然后主线程将其唤醒,并将该值一次复制到每个子线程。正如您在问题中所说,在循环的顶部,子线程可以检查其特定于线程的队列,以查看是否存在其他边界条件

实际上,您不需要在本教程中使用主线程。您可以让孩子们直接写入所有其他孩子们的队列,但仍然受互斥保护,只要您小心避免死锁,它也会这样工作

所有这些类都可以在线程支持库中看到,并提供了适当的文档


是的,有一些基于中断的方法,但在这种情况下,轮询相对便宜,因为在一个互斥体上没有太多的线程,但主要是线程特定的互斥体,互斥体快速锁定、检查和解锁并不那么昂贵。你没有长时间的坚持,所以这没关系。这真的有点像一个测试:你需要无锁的额外复杂性吗?如果它只有十几个或更少的线程,那么可能不会。

这实际上非常简单。你只需要知道事情是如何运作的,就可以确信简单的解决方案不会被打破。因此,您需要两件事: 1.确保每次访问变量时都将其写入/读取内存。 2.确保以原子方式读取,这意味着您必须一次性读取完整值,或者如果不是自然读取,则进行廉价测试以验证。 要处理1,您必须声明它为volatile。确保volatile关键字应用于变量本身。不是那样的。 要解决2,它取决于类型。在x86/64上,对整数类型的访问是原子性的,只要它们与大小一致。也就是说,int32_t必须与4位边界对齐,int64_t必须与8字节边界对齐。 所以你可能有这样的想法:

如果bounds变量在结构中更复杂,但仍适合64位,则可以将其与uint64_t合并,并使用与上述相同的属性和volatile。 如果它对于64位来说太大,您将需要某种锁来确保您没有读取半陈旧的值。对于您的环境,单编写器、多个读卡器的最佳锁定是序列锁定。序列锁只是一个volatile int,如上所述,用作数据的版本。它的值从0开始,并在每次更新时提前2。在更新受保护的值之前,将其递增1,然后再递增1。最终结果是偶数是稳定状态,奇数是瞬时值更新。在读卡器中,您可以执行以下操作: 1.阅读版本。如果未更改-返回 2.读到你得到一个偶数 3.读取受保护的变量 4.再读一遍这个版本。如果你得到 和以前一样,你很好 5.否则-返回到步骤2 这实际上是我下一篇文章的主题之一。我将在C++中实现并让你知道。同时,您可以查看linux内核中的seqlock。 另一个注意事项是,在内存访问之间需要设置编译器屏障,这样编译器就不会对本来不应该进行的事情重新排序。在gcc中,您就是这样做的:


是只有一个int需要通信,还是有一个完整的数据结构?在你的例子中,我认为一个好的选择是原子式的,内存顺序是宽松的。因此使用val.loadmemory_order_轻松;和val.storenewval,内存顺序松弛;。原子保护您不受未定义行为的影响,而内存屏障在保持写/读操作原子化的同时避免了巨大的性能影响,如“不可分割”中所示。如果有新的边界可用,为什么必须频繁地进行从机检查?特别是当另一个线程只是偶尔遇到一个新的绑定时?请注意,一个线程不能依赖另一个线程查找绑定线程,因为绑定线程可以以任何顺序运行,因此这些绑定只能作为优化。可供探索的搜索空间更小。我同意记忆的顺序,放松——偶尔错过一次也没关系。我不确定的一件事是,你的界是一个单原子int还是更多。因为一对原子int不是一对原子int。从std::atomic读取时,除了编译器优化之外,x86上的开销为零。如果您需要更有针对性的建议,我建议您提供有关应用程序的更多信息,例如代码的相关部分、正在运行的硬件,特别是线程数、问题大小、性能要求……我会说得更有力一些。如果int在您的平台上是原子的,那么使用原子将是便宜的。当原子是昂贵的,使用int作为原子将是错误的。所以就用原子。它不仅是安全的,而且是最快的安全解决方案。您没有抓住要点:问题不在于对int的写入可能不是原子的,您可能会看到部分更新。问题是,如果编译器不影响单线程程序的正确执行,那么它可以优化掉对非原子变量的任何读写操作。例如,编译器可以将全局非原子变量上的自旋锁转换为无止境循环或nop,具体取决于该变量的初始状态。特别是对bounds变量的写入不能保证发布到其他线程。事实上,我非常清楚这一点,使用volatile关键字可以避免这种优化。那么也许你可以在回答中提到这一点?不是每个读过你文章的人都知道这一点。但是,当你可以轻松解决原子问题时,为什么还要有互斥锁等开销呢?因为如果有多个边界条件呢?您不只是对所有线程的最新更新感兴趣,而是对所有线程都感兴趣。使用原子只能让线程看到最新的值,这可能还不够。@Kevin Anderson,我认为在这种情况下,我们只关心最后一个状态,而不是所有状态。与单个编写器场景相比,互斥将有巨大的开销。@BitWhistler-如果是真的,那么我同意,原子可能是最好的。但是如果需要所有的值,那么您需要更多的值。虽然可以说,如果你有多个编写器,每个线程都可以写,那么如果两个线程读40,一个线程有35个线程要写,一个线程有37个线程呢?即使在那时,你仍然会遇到问题。35可能是正确的,但37可能会被写进去。你仍然使用原子,但是你也需要使用函数。这个答案是错误的。x86/64访问整数类型时的语句是原子的,只要它们与大小一致。是完全错误的。@fsaintjacques,在作出此类评论之前,您应该阅读《英特尔软件开发人员手册》第3卷:8.2.3.1中的“关于多处理器计算机中的内存排序”部分,“英特尔64内存排序模型”保证,对于以下每个内存访问指令,组成内存操作似乎作为单个内存访问执行:读取或写入地址在4字节边界上对齐的双字4字节的指令读取或写入地址在8字节边界上对齐的四字8字节的指令。对齐存储、易失性、小于64位的类型和手动内存屏障的组合将完成这项工作,这是正确的,但也是最糟糕的、容易出错的方法。只需使用c++11的原子或本质。它们都比你的构造更有效,也更不容易出错。@MikeMB,最糟糕的方法是不知道事情是如何运作的。如果您想要一个快速的低级实现,那么您必须知道事情是如何工作的,然后使用asm、intrinsics或c++11原子并不重要。C++11不会提高效率,因为它们只是编译器内部函数或asm的包装器。我会看看那些普通人 ted asm验证是否没有冗余锁或cmp/exch。@BitWhistler:…大概是最坏的情况。。。可能太苛刻了,对此我很抱歉。但是,即使是有经验的程序员也会犯错误,而且他们需要编写和审阅的代码越多,错误就越有可能存在,即使程序员——理论上——知道指令的作用以及它们如何交互。b它是不可移植的,甚至从标准的角度来看可能是UB c你可能不需要一个完整的编译器围栏,所以d这是不可能的,但是你可能会禁用有效的编译器优化,这会使你的代码更快。
struct Params {
  volatile uint64_t bound __attribute__((aligned(8)));
};
asm volatile ("":::"memory");