下面的代码用于将工作分配给多个线程,将其唤醒,并等待它们完成 . 在这种情况下,“工作”包括“清理卷” . 这个操作究竟做了什么与这个问题无关 - 它只是有助于上下文 . 该代码是巨大的事务处理系统的一部分 .
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( EDIT :此解决方案确实有效 . 请参阅下面的编辑)
许多专家说volatile与线程同步无关,它只应用于低级硬件访问 . 但是在互联网上存在很多争议 . 我理解它的方式,volatile是唯一的方法来阻止编译器优化对当前作用域之外更改的内存访问,而不管并发访问 . 从这个意义上说,即使我们不同意并发编程的最佳实践,volatile也应该可以解决问题 .
- 介绍内存栅栏
方法 wakeup_cleaners()
在内部获取 pthread_mutex_t
以便在工作线程中设置唤醒标志,因此它应隐式生成适当的内存屏障 . 但我不确定这些围栏是否影响调用方法中的内存访问( force_all()
) . 因此,我在上面的注释指定的位置手动引入了围栏 . 这应该确保_2462104中的工作线程执行的写操作在主线程中可见 .
令我困惑的是,这些解决方案都不起作用,我完全不知道为什么 . 内存栅栏和易失性的语义和正确使用让我感到困惑 . 问题是编译器正在应用不需要的优化 - 因此是易失性尝试 . 但它也可能是线程同步的问题 - 因此内存栅栏尝试 .
我可以尝试第三个解决方案,其中互斥锁保护对 _requested_volumes
的每次访问,但即使这样有效,我也想理解为什么,因为据我所知,它通过互斥锁显式或隐式地完成了.2462106_s .
EDIT: 我的假设是错误的,解决方案1确实有效 . 但是,我的问题仍然是为了澄清volatile与内存栅栏的使用 . 如果volatile是如此糟糕的事情,那绝不应该用于多线程编程,我还应该在这里使用什么呢?内存栅栏是否也会影响编译器优化?因为我认为这些是两个正交问题,因此也就是正交解决方案:在多个线程中可见性的范围和用于防止优化的易失性 .
2 回答
是 .
通常,不是“专家”之间 .
不 .
非纯的,非constexpr非内联函数调用(getter / accessors)也必然具有这种效果 . 无可否认,链接时优化会混淆哪些功能可能真正被内联的问题 .
在C中,通过扩展C,
volatile
会影响内存访问优化 . Java使用了这个关键字,因为它可以在第一时间执行C使用volatile
的任务,将其更改为提供内存栅栏 .在C中获得相同效果的正确方法是使用
std::atomic
.不,它可能会产生预期的效果,具体取决于它与您的平台的交互方式's cache hardware. This is brittle - it could change any time you upgrade a CPU, or add another one, or change your scheduler behaviour - and it certainly isn' t portable .
如果您真的只是跟踪有多少工作人员仍在工作,那么理智的方法可能是信号量(同步计数器)或互斥量condvar整数计数 . 两者都可能比忙于循环睡眠更有效 .
如果你已经忙于繁忙的循环,你仍然可以合理地拥有一个计数器,例如
std::atomic<size_t>
,由wakeup_cleaners
设置,并在每个清洁器完成时递减 . 然后你可以等它达到零 .如果你真的想要一个繁忙的循环并且每次都非常喜欢扫描数组,它应该是
std::atomic<bool>
的数组 . 通过这种方式,您可以确定每个加载所需的一致性,并且它将适当地控制编译器优化和内存硬件 .显然,
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