下面的代码用于将工作分配给多个线程,唤醒它们,并等待它们完成。在这种情况下,“工作”包括“清理卷”。这个操作到底做了什么与这个问题无关——它只是有助于上下文。该代码是一个庞大的事务处理系统的一部分。
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
始终为真,即使数组中的所有值都已设置为假。这只发生在-O3
.
我尝试了两种解决方案来解决这个问题:
- 声明
_requested_volumes
易失性(编辑:此解决方案确实有效。请参阅下面的编辑)
很多专家表示,volatile与线程同步无关,应该只用在低级硬件访问中。但网上对此有很多争议。我理解它的方式是, volatile 是避免编译器优化对在当前范围之外更改的内存的访问的唯一方法,无论并发访问如何。从这个意义上说,volatile应该可以解决问题,即使我们对并发编程的最佳实践存在分歧。
- 引入内存栅栏
该方法在内部wakeup_cleaners()
获取 apthread_mutex_t
以便在工作线程中设置唤醒标志,因此它应该隐式生成适当的内存栅栏。但我不确定这些栅栏是否会影响调用方方法 ( force_all()
) 中的内存访问。因此,我在上面注释指定的位置手动引入了栅栏。这应该确保工作线程执行的写入_requested_volumes
在主线程中可见。
令我困惑的是,这些解决方案都不起作用,我完全不知道为什么。内存栅栏和易失性的语义和正确使用现在让我感到困惑。问题是编译器正在应用不需要的优化——因此是不稳定的尝试。但这也可能是线程同步的问题——因此是内存栅栏尝试。
我可以尝试第三种解决方案,其中互斥锁保护_requested_volumes
对 . 因此,无论是通过互斥体显式还是隐式完成,都应该没有区别。
编辑:我的假设是错误的,解决方案 1确实有效。但是,我的问题仍然是为了澄清易失性与内存围栏的使用。如果 volatile 是一件坏事,那不应该在多线程编程中使用,我还应该在这里使用什么?内存栅栏也会影响编译器优化吗?因为我认为这是两个正交的问题,因此也是正交的解决方案:用于多线程可见性的栅栏和用于防止优化的 volatile。