代码之家  ›  专栏  ›  技术社区  ›  Michael Ekstrand

为什么在多线程C或C++编程中,波动性不被认为有用?

  •  149
  • Michael Ekstrand  · 技术社区  · 16 年前

    this answer 我最近发布了一篇文章,我似乎对 volatile 在多线程编程环境中。

    我的理解是:任何时候一个变量可能在访问它的代码的控制流之外被更改,这个变量应该被声明为 不稳定的

    foo 由一个线程读取并由另一个线程原子地设置(可能使用适当的机器指令),读取线程看到这种情况的方式与它看到由信号处理程序调整或由外部硬件条件修改的变量的方式相同,因此 应申报

    我错在哪里?

    9 回复  |  直到 9 年前
        1
  •  223
  •   Lightness Races in Orbit    11 年前

    问题在于 volatile 在多线程上下文中,它不提供 我们需要的保证。它确实有一些我们需要的属性,但不是全部,所以我们不能依赖它 不稳定的 单独地

    但是,我们必须为 剩下的 属性还提供 不稳定的 是的,所以实际上是没有必要的。

    对于共享数据的线程安全访问,我们需要保证:

    • 不会重新排序。假设我们使用 不稳定的 变量作为一个标志,指示某些数据是否准备好读取。在我们的代码中,我们只需在准备好数据后设置标志,所以 第一 ?

    保证了第一点。它还保证不会发生重新排序 在不同的volatile读/写之间 . 全部 不稳定的 不稳定的 不稳定的 不稳定的 一个。

    防止重新排序的解决方案是使用 记忆障碍 不能跨此点重新排序内存访问 . 在易失性变量访问周围设置这样的屏障,可以确保即使是非易失性访问也不会跨易失性变量重新排序,从而允许我们编写线程安全的代码。

    然而,记忆障碍 不稳定的 没必要。我们可以把它移走 不稳定的 完全限定符。

    自从C++11以来,原子变量( std::atomic<T> )给我们所有的相关保证。

        2
  •  52
  •   Community Mohan Dere    7 年前

    你也可以从 Linux Kernel Documentation .

    C程序员经常把volatile理解为变量 可以在当前执行线程之外更改;作为一个 结果,他们有时会尝试在内核代码中使用它 正在使用共享数据结构。换句话说,他们已经 已知将易失性类型视为一种简单的原子变量 对的;本文介绍了原因。

    了解volatile的关键点是 真的很想做。在内核中,必须保护共享数据 结构来防止不必要的并发访问,这是一个非常重要的问题 不同的任务。防止不必要的伤害的过程 并发还可以避免几乎所有与优化相关的问题 以更有效的方式。

    数据安全(自旋锁、互斥锁、内存屏障等)旨在 也不需要使用volatile。如果挥发性物质仍然存在 必要的是,几乎可以肯定代码中的某个地方有一个bug。在 趴下。

    考虑一个典型的内核代码块:

    spin_lock(&the_lock);
    do_something_on(&shared_data);
    do_something_else_with(&shared_data);
    spin_unlock(&the_lock);
    

    无法在持有\ U锁时意外更改。任何其他代码 可能想玩这些数据的人会在锁上等待。 spinlock原语充当内存屏障-它们是显式的 穿过他们。所以编译器可能认为它知道 障碍,将迫使它忘记它所知道的一切。不会再有了 数据访问的优化问题。

    如果共享的数据被声明为volatile,那么锁定仍然是volatile 必要的。但是编译器也会被阻止优化 共享数据访问 在内部 没人能用它。当锁被锁住的时候, 锁定使得volatile变得不必要——而且可能有害。

    寄存器。在内核中,寄存器访问也应该是 受锁保护,但也不希望编译器 在内核中,I/O内存访问总是通过访问器完成的 不适用于所有体系结构。那些访问者是

    另一种可能会使用volatile的情况是 处理器正忙着等待变量的值。右边 执行繁忙等待的方法是:

    while (my_variable != what_i_want)
        cpu_relax();
    

    cpu_relax()调用可以降低cpu功耗,或者让cpu 障碍,所以,再次,挥发性是没有必要的。当然,

    目前仍有一些罕见的情况下,volatile是有意义的 内核:

    • 上述访问器函数可能使用volatile on 直接I/O内存访问工作的体系结构。基本上,

    • 内联汇编代码,它改变内存,但没有其他内存 关键字到asm语句将阻止此删除。

    • jiffies变量的特殊之处在于它可以有不同的值 锁定。所以jiffies可以是不稳定的,但是 在这方面是一个“愚蠢的遗产”问题(莱纳斯的话);修好它

    • 指向相干内存中可能被修改的数据结构的指针 通过I/O设备,有时可以合法地是易失性的。环形缓冲器 指出哪些描述符已被处理,这是一个例子

    因此,volatile的使用很可能被视为一个bug和 将对代码进行额外的审查。开发人员 尝试使用volatile的人应该退一步想想 他们真的在努力实现。

        3
  •  12
  •   Jeremy Friesner    16 年前

    混乱是因为volatile不足以实现许多事情。特别是,现代系统使用多级缓存,现代多核cpu在运行时进行一些奇特的优化,现代编译器在编译时进行一些奇特的优化,这些都会导致各种副作用以不同的顺序出现,而这些顺序与您只看源代码时所期望的顺序不同。

    就我个人而言,volatile标志的主要用途是“plessegoawaynow”布尔值。如果我有一个连续循环的工作线程,我会让它在循环的每次迭代中检查volatile boolean,如果boolean为真就退出。然后,主线程可以安全地清理工作线程,方法是将布尔值设置为true,然后调用pthread\u join()等待工作线程消失。

        4
  •  9
  •   Potatoswatter    16 年前

    volatile .

    多线程编程的典型方法不是在机器级保护每个共享变量,而是引入引导程序流的保护变量。而不是 volatile bool my_shared_flag;

    pthread_mutex_t flag_guard_mutex; // contains something volatile
    bool my_shared_flag;
    

    这不仅封装了“硬部分”,而且从根本上说是必要的:C不包括 原子操作 实现互斥所必需的;它只有 普通的 操作。

    现在你有了这样的东西:

    pthread_mutex_lock( &flag_guard_mutex );
    my_local_state = my_shared_flag; // critical section
    pthread_mutex_unlock( &flag_guard_mutex );
    
    pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
    my_shared_flag = ! my_shared_flag; // critical section
    pthread_mutex_unlock( &flag_guard_mutex );
    

    my_shared_flag

    1. 这意味着对它的引用必须是在某个时候(与 & 操作员)。
    2. pthread_mutex_lock 是一个库函数。
    3. 这意味着编译器无法判断 pthread\u mutex\u锁 不知何故得到了那个参照物。
    4. 意味着编译器必须 那个 修改共享标志 !
    5. 所以必须从内存中重新加载变量。 不稳定的 虽然在这种情况下有意义,但却是无关的。
        5
  •  7
  •   jpalecek    16 年前

    你的理解真的错了。

    volatile变量的属性是“对这个变量的读写是程序可感知行为的一部分”。这意味着该程序可以工作(给定适当的硬件):

    int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
    *reg=1; // turn the fuel on
    *reg=2; // ignition
    *reg=3; // release
    int x=*reg; // fire missiles
    

    例如,线程安全计数器应该是(类似于linux内核的代码,不知道c++0x等价物):

    atomic_t counter;
    
    ...
    atomic_inc(&counter);
    

    这是原子的,没有记忆障碍。如果需要的话,你应该添加它们。添加volatile可能不会有帮助,因为它不会关联到对附近代码的访问(例如,将元素附加到计数器正在计数的列表中)。当然,您不需要看到计数器在您的程序之外递增,而且优化仍然是可取的,例如。

    atomic_inc(&counter);
    atomic_inc(&counter);
    

    atomically {
      counter+=2;
    }
    

    如果优化器足够聪明(它不会改变代码的语义)。

        6
  •  6
  •   zebrabox    16 年前

    1) 原子性,也就是说,如果我读或写一些数据到内存中,那么这些数据在一个过程中被读/写,并且不能由于上下文切换而被中断或争用

    看到 在多个并发环境之间是相同的,比如线程、机器等

    无论是上述的还是更特别的,无论是C还是C++标准,对于挥发性如何表现都不包括上述两种。

    在实践中更糟糕的是,有些编译器(如英特尔安腾编译器)确实试图实现某些并发访问安全行为元素(即通过确保内存限制),但是编译器实现之间没有一致性,而且标准一开始并不要求实现这一点。

    将变量标记为volatile只意味着每次都要强制将值刷新到内存中或从内存中刷新,这在许多情况下会降低代码的速度,因为基本上已经破坏了缓存性能。

    c#和javaafaik通过使volatile遵守1)和2)来纠正这一点,但是对于c/c++编译器来说,情况并非如此,因此基本上可以根据您的需要使用volatile。

    关于这个问题的更深入的(尽管不是无偏见的)讨论,请阅读 this

        7
  •  6
  •   Tony Delroy    10 年前

    comp.programming.threads常见问题解答 a classic explanation 作者:Dave Butenhof:

    问题56:为什么我不需要声明共享变量VOLATILE?

    但是,我担心编译器和 线程库满足各自的规范。一致的 C编译器可以全局分配一些共享(非易失性)变量给 线对线。每个线程都有自己的私有值 这个共享变量不是我们想要的共享变量

    在某种意义上,如果编译器对 变量和pthread\u cond\u wait(或 pthread\u mutex\u lock)函数。实际上,大多数编译器不会尝试 在调用外部数据库时保留全局数据的注册副本 函数,因为很难知道例程是否

    所以是的,一个编译器确实严格遵守 不稳定的。但最好有人来修。因为任何系统(即, 实际上,内核、库和C编译器的组合 符合POSIX标准。句号。系统不能要求您使用 只要求POSIX同步功能是必需的。

    所以如果你的程序因为没有使用volatile而中断,那就是一个BUG。 内核。但这是一个系统错误,以及其中一个或多个组件 得努力才能修好。

    你不想使用volatile,因为,在任何一个系统上 不管有什么不同,它都会比一辆普通的汽车贵得多 变量,而POSIX只在 总之,是记忆活动让你放慢了速度。)

    /---[戴夫·布滕霍夫]--------------------------[butenhof@zko.dec.com ]---\

    |603.881.2218,传真603.881.0120 Nashua NH 03062-2698|
    -----------------[通过并发更好地生活]----------------/

    布滕霍夫先生在这方面也有很多相同的见解 this usenet post :

    使用“volatile”不足以确保正确的内存 线程之间的可见性或同步。互斥的使用是 足够,并且,除非借助各种非便携式机器 更难普遍应用的规则,如中所述 我以前的文章),互斥是必要的。

    因此,正如Bryan所解释的,volatile的使用完成了 安全”。当然,欢迎你申报你想要的任何东西 不要指望它能为您解决任何线程同步问题。

        8
  •  3
  •   Zack Yezek    12 年前

    这就是“volatile”所做的一切: “嘿,编译器,这个变量可以在任何时刻(在任何时钟滴答声上)改变,即使没有本地指令作用于它。不要在寄存器中缓存此值。“

    您可能会遇到像“Dobbs博士”这样的文章,这些文章被认为是多线程编程的灵丹妙药。他的方法并非完全没有优点,但它有一个根本的缺陷,即让对象的用户对其线程安全负责,这往往与其他违反封装的行为有相同的问题。

        9
  •  3
  •   david    11 年前

    有选择有“不稳定”的意思吗 “多进程环境中的线程安全访问” . 但他们没有。

    这意味着关键代码段周围的“volatile”信号量之类的东西,在新的硬件和新的编译器上不起作用,可能曾经在旧的硬件上和旧的编译器起作用,而旧的例子有时不是错的,只是旧的。