下面的代码用于将工作分配给多个线程,将其唤醒,并等待它们完成。在这种情况下,“工作”包括“清理卷”。这个操作究竟做了什么与这个问题无关 - 它只是有助于上下文。该代码是巨大的事务处理系统的一部分。
void bf_tree_cleaner::force_all()
{
for (int i = 0; i < vol_m::MAX_VOLS; i++) {
_requested_volumes[i] = true;
}
// fence here (seq_cst)
wakeup_cleaners();
while (true) {
usleep(10000); // 10 ms
bool remains = false;
for (int vol = 0; vol < vol_m::MAX_VOLS; ++vol) {
// fence here (seq_cst)
if (_requested_volumes[vol]) {
remains = true;
break;
}
}
if (!remains) {
break;
}
}
}
布尔数组_requested_volumes[i]
中的值告诉线程i
是否有工作要做。完成后,工作线程将其设置为false并返回休眠状态。
我遇到的问题是编译器生成一个无限循环,其中变量remains
始终为true,即使数组中的所有值都已设置为false。这只发生在-O3
。
我尝试了两种解决方案来解决这个问题:
_requested_volumes
不稳定
(编辑:此解决方案确实有效。请参阅下面的编辑)许多专家说volatile与线程同步无关,它只应用于低级硬件访问。但是在互联网上存在很多争议。我理解它的方式,volatile是阻止编译器优化对当前作用域之外的内存访问的唯一方法,无论并发访问。从这个意义上说,即使我们不同意并发编程的最佳实践,volatile 也应该做到这一点。
方法wakeup_cleaners()
在内部获取pthread_mutex_t
以便在工作线程中设置唤醒标志,因此它应隐式生成适当的内存屏障。但我不确定这些围栏是否会影响调用方法(force_all()
)中的内存访问。因此,我在上面的注释指定的位置手动引入了围栏。这应该确保_requested_volumes
中的工作线程执行的写操作在主线程中可见。
让我感到困惑的是,这些解决方案都不起作用,我完全不知道为什么。内存栅栏和易失性的语义和正确使用让我感到困惑。问题是编译器正在应用不需要的优化 - 因此是易失性尝试。但它也可能是线程同步的问题 - 因此内存栅栏尝试。
我可以尝试第三个解决方案,其中互斥锁保护对_requested_volumes
的每次访问,但即使这样可行,我也想了解原因,因为据我所知,这都是关于内存栅栏的。因此,无论是通过互斥锁明确地还是隐式地完成它都应该没有区别。
编辑:我的假设是错误的,而解决方案1实际上 工作。但是,我的问题仍然是为了澄清volatile与内存栅栏的使用。如果volatile是如此糟糕的事情,那绝不应该用于多线程编程,我还应该在这里使用什么呢?内存栅栏是否也会影响编译器优化?因为我认为这些是两个正交问题,因此也就是正交解决方案:在多个线程中提供可见性的范围,以及用于防止优化的易失性。
答案 0 :(得分:4)
许多专家说volatile与线程同步无关,它只能用于低级硬件访问。
是
但是在互联网上存在很多争议。
一般而言,不是专家之间的#34;
我理解它的方式,volatile是阻止编译器优化对当前作用域之外更改的内存访问的唯一方法,无论并发访问如何。
不。
非纯,非constexpr非内联函数调用(getters / accessors)也必然具有此效果。不可否认,链接时优化会混淆哪些函数可能真正被内联的问题。
在C中,通过扩展C ++,volatile
会影响内存访问优化。 Java使用了这个关键字,因为它不能(或者不能)执行C首先使用volatile
的任务,所以改变它以提供内存栅栏。
在C ++中获得相同效果的正确方法是使用std::atomic
。
从这个意义上说,即使我们不同意并发编程的最佳实践,volatile也应该可以解决问题。
不,可能具有所需的效果,具体取决于它与您平台的缓存硬件的交互方式。这很脆弱 - 它可能会在您升级CPU或添加另一个CPU或更改您的调度程序行为时随时更改 - 而且它当然不可移植。
如果您真的只是跟踪有多少工作者仍然在工作,那么理智的方法可能是信号量(同步计数器),或互斥+ condvar +整数计数。两者都可能比忙于循环睡眠更有效。
如果您已经忙于忙碌循环,您仍然可以合理地拥有一个单个计数器,例如std::atomic<size_t>
,由wakeup_cleaners
设置并递减每个清洁工完成。然后你可以等它达到零。
如果你真的想要一个繁忙的循环并且每次都更喜欢扫描数组,它应该是std::atomic<bool>
的数组。通过这种方式,您可以确定每个加载所需的一致性,并且它将适当地控制编译器优化和内存硬件。
答案 1 :(得分:1)
显然,volatile
为您的示例做了必要的事情。 volatile
限定符本身的主题过于宽泛:您可以先搜索“C++ volatile vs atomic”等。互联网上有很多文章,问题和答案,例如: Concurrency: Atomic and volatile in C++11 memory model。
简而言之,volatile
告诉编译器禁用一些积极的优化,特别是每次访问时读取变量(而不是将其存储在寄存器或缓存中)。有些编译器可以做得更多,使volatile
更像std::atomic
:请参阅Microsoft特定部分here。在您的情况下,禁用激进优化正是必要的。
但是,volatile
没有定义围绕它执行语句的顺序。这就是为什么你需要memory order,以防你在设置了标志后需要对数据执行其他操作。
对于线程间通信,使用std::atomic
是合适的,特别是,您需要将_requested_volumes[vol]
重构为std::atomic<bool>
类型甚至std::atomic_flag
:http://en.cppreference.com/w/cpp/atomic/atomic。
一篇不鼓励使用volatile的文章,并解释volatile只能在极少数特殊情况下使用(与硬件I / O连接):https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt