Cae*_*uer 1 c++ multithreading gcc volatile memory-fences
下面的代码用于将工作分配给多个线程,唤醒它们,并等待它们完成。在这种情况下,“工作”包括“清理卷”。这个操作到底做什么与这个问题无关——它只是对上下文有帮助。该代码是庞大事务处理系统的一部分。
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
布尔数组中的值_requested_volumes[i]表明线程是否i有工作要做。完成后,工作线程将其设置为 false 并返回睡眠状态。
我遇到的问题是编译器生成一个无限循环,其中变量remains始终为 true,即使数组中的所有值都已设置为 false。这只发生在-O3.
我尝试了两种解决方案来解决这个问题:
_requested_volumes易失性(编辑:这个解决方案确实有效。请参阅下面的编辑)许多专家表示,挥发性与线程同步无关,它应该只用于低级硬件访问。但网上对此争议颇多。根据我的理解,无论并发访问如何,易失性是阻止编译器优化对当前范围之外更改的内存的访问的唯一方法。从这个意义上说,即使我们不同意并发编程的最佳实践,但volatile应该可以解决问题。
该方法在内部wakeup_cleaners()获取 apthread_mutex_t以便在工作线程中设置唤醒标志,因此它应该隐式地产生适当的内存栅栏。但我不确定这些栅栏是否会影响调用者方法(force_all())中的内存访问。因此,我在上面注释指定的位置手动引入了围栏。这应该确保工作线程执行的写入_requested_volumes在主线程中可见。
令我困惑的是,这些解决方案都不起作用,我完全不知道为什么。内存栅栏和易失性的语义和正确使用现在让我感到困惑。问题在于编译器正在应用不需要的优化——因此是不稳定的尝试。但这也可能是线程同步的问题——因此内存栅栏尝试。
我可以尝试第三种解决方案,其中互斥体保护对 的每次访问_requested_volumes,但即使这有效,我也想了解为什么,因为据我所知,这都是关于内存栅栏的。因此,无论是通过互斥体显式还是隐式完成,都应该没有区别。
编辑:我的假设是错误的,解决方案 1 实际上有效。然而,我的问题仍然是为了澄清易失性与内存栅栏的使用。如果 volatile 是个坏东西,永远不应该在多线程编程中使用,那么我在这里还应该使用什么?内存栅栏也会影响编译器优化吗?因为我将这些视为两个正交问题,因此也是正交解决方案:用于多线程中可见性的栅栏和用于防止优化的易失性。
许多专家表示,挥发性与线程同步无关,它应该只用于低级硬件访问。
是的。
但网上对此争议颇多。
一般来说,不是在“专家”之间。
按照我的理解,无论并发访问如何,易失性是阻止编译器优化对当前范围之外更改的内存的访问的唯一方法。
没有。
非纯、非 constexpr 非内联函数调用(getters/accessors)也必然具有这种效果。诚然,链接时优化混淆了哪些函数可能真正内联的问题。
在 C 以及扩展的 C++ 中,volatile会影响内存访问优化。Java 采用了这个关键字,并且由于它不能(或不能)执行 Cvolatile最初使用的任务,因此对其进行了更改以提供内存栅栏。
在 C++ 中获得相同效果的正确方法是使用std::atomic.
从这个意义上说,即使我们不同意并发编程的最佳实践,但 volatile 应该可以解决问题。
不,它可能会产生预期的效果,具体取决于它与平台的缓存硬件的交互方式。这是脆弱的 - 它可能会在您升级 CPU、添加另一个 CPU 或更改调度程序行为时发生变化 - 而且它当然不可移植。
如果您真的只是跟踪有多少工作人员仍在工作,那么明智的方法可能是信号量(同步计数器)或互斥体+条件变量+整数计数。两者都可能比忙循环睡眠更有效。
如果您习惯于繁忙的循环,您仍然可以合理地拥有一个计数器,例如std::atomic<size_t>,它由每个清理器完成时设置wakeup_cleaners并递减。然后你就可以等待它达到零。
如果您确实想要一个繁忙的循环并且确实更喜欢每次扫描数组,那么它应该是一个std::atomic<bool>. 这样您就可以决定每次加载所需的一致性,并且它将适当地控制编译器优化和内存硬件。
| 归档时间: |
|
| 查看次数: |
1704 次 |
| 最近记录: |