首页 文章

易失性与内存栅栏

提问于
浏览
1

下面的代码用于将工作分配给多个线程,将其唤醒,并等待它们完成 . 在这种情况下,“工作”包括“清理卷” . 这个操作究竟做了什么与这个问题无关 - 它只是有助于上下文 . 该代码是巨大的事务处理系统的一部分 .

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 回答

  • 1

    许多专家说volatile与线程同步无关,它只应用于低级硬件访问 .

    是 .

    但是在互联网上存在很多争议 .

    通常,不是“专家”之间 .

    我理解它的方式,volatile是阻止编译器优化对当前作用域之外更改的内存访问的唯一方法,无论并发访问如何 .

    不 .

    非纯的,非constexpr非内联函数调用(getter / accessors)也必然具有这种效果 . 无可否认,链接时优化会混淆哪些功能可能真正被内联的问题 .

    在C中,通过扩展C, volatile 会影响内存访问优化 . Java使用了这个关键字,因为它可以在第一时间执行C使用 volatile 的任务,将其更改为提供内存栅栏 .

    在C中获得相同效果的正确方法是使用 std::atomic .

    从这个意义上讲,即使我们不同意并发编程的最佳实践,volatile也应该可以解决问题 .

    不,它可能会产生预期的效果,具体取决于它与您的平台的交互方式'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> 的数组 . 通过这种方式,您可以确定每个加载所需的一致性,并且它将适当地控制编译器优化和内存硬件 .

  • 4

    显然, 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_flaghttp://en.cppreference.com/w/cpp/atomic/atomic .

    一篇不鼓励使用volatile的文章,并解释了volatile只能在极少数特殊情况下使用(与硬件I / O连接):https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt

相关问题