这里是否需要 volatile

Is volatile required here

我正在实施 'sequence lock' class 以允许数据结构的锁定写入和无锁定读取。

将包含数据的结构包含序列值,该序列值将在写入时递增两次。写作开始前一次,写作完成后一次。作者在 reader(s) 之外的其他线程上。

这是保存数据副本的结构,序列值如下所示:

template<typename T>
struct seq_data_t
{
    seq_data_t() : seq(0) {};
    int seq;                     <- should this be 'volatile int seq;'?
    T data;
};

整个序列锁class在循环缓冲区中持有该结构的N个副本。写入线程总是覆盖循环缓冲区中最旧的数据副本,然后将其标记为当前副本。写入被互斥锁定。

读取功能不锁定。它尝试读取数据的 'current' 副本。它在读取之前存储 'seq' 值。然后它读取数据。然后它再次读取 seq 值,并将其与第一次读取的值进行比较。如果seq值没有变化,则认为read是好的。

由于写入线程可以在读取发生时更改 'seq' 的值,我认为 seq 变量应该标记为 volatile,以便读取函数将显式读取它之后的值读取数据。

读函数看起来是这样的:它会在 writer 以外的线程上,也许是几个线程。

    void read(std::function<void(T*)>read_function)
    {
        for (;;)
        {
            seq_data_type<T>* d = _data.current; // get current copy
            int seq1 = d->seq;      // store starting seq no
            if (seq1 % 2)           // if odd, being modified...
                continue;           //     loop back

            read_function(&d->data);  // call the passed in read function
                                      // passing it our data.


//??????? could this read be optimized out if seq is not volatile?
            int seq2 = d->seq;      // <-- does this require that seq be volatile?
//???????

            if (seq1 == seq2)       // if still the same, good.
                return;             // if not the same, we will stay in this
                                    // loop until this condition is met.
        }
    }

问题:

1) seq 在这种情况下必须是可变的吗?

2) 在具有多个成员的结构的上下文中,只有 volatile 限定变量是 volatile,而不是其他成员?即如果我只在结构中将其标记为易失性,则仅 'seq' 易失性?

1) must seq be volatile in this context?

当然,从 seq 读取的数据很可能会被 -O3 优化掉。所以是的,您应该提示编译器 seq 可能会在其他地方(即在其他线程中)使用 volatile 关键字进行更改。

对于 x86 架构就足够了,因为 x86 内存模型(几乎)是顺序的,如 on Wikipedia 所述。

为了可移植性,最好使用原子原语。

2) in the context of a struct with multiple members, are only the volatile qualified variable volatile, and not the other members? i.e. is only 'seq' volatile if I only mark it volatile within the struct?

不,data 也应该标记为易变的(或者您也应该使用原子原语)。基本上,循环:

for (;;) {
    seq1 = d->seq;
    read_data(d->data);
    seq2 = d->seq;
    if (seq1 == seq2)
        return;
}

相当于:

read_data(d->data);
return;

因为代码中唯一可观察到的效果是 read_data() 调用。

请注意,使用 -O3 编译器很可能会对您的代码进行相当广泛的重新排序。因此,即使对于 x86 架构,您也需要在第一次 seq 读取、data 读取和第二次 seq 读取之间设置编译器屏障,即:

for (;;)
    {
        seq_data_type<T>* d = _data.current;
        int seq1 = d->seq;
        COMPILER_BARRIER();
        if (seq1 % 2)
            continue;

        read_function(&d->data);
        COMPILER_BARRIER();
        int seq2 = d->seq;
        if (seq1 == seq2)
            return;
    }
}

最轻量级的编译器障碍是:

 #define COMPILER_BARRIER asm volatile("" ::: "memory")

对于 C++11,您可以使用 atomic_signal_fence()

总的来说,使用 std::atomic<> 更安全:它更便携,而且不像处理 volatiles 和编译器障碍那样棘手...

另请查看 Herb Sutter 的名为 "atomic<> Weapons" 的演示文稿,其中解释了编译器和其他内存屏障以及原子:https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2

如果代码是可移植的,volatile 永远不合适,除非处理内存映射硬件。我再说一遍,从不 合适。 Microsoft Visual C++(x86 或 x86/64)使用默认的编译器标志,添加了一些标准中没有的内存顺序保证。因此,使用该编译器并启用非标准行为,volatile 可能适用于某些多线程操作。

使用标准的多线程支持,例如std::atomic、std::mutex、std::condition_variable等

实际问题是,在写入某些内存(在本例中为data)时,它被描述为数据竞争,因此程序的行为是不确定的。即使您使 seq 成为原子,从 data 读取仍会导致数据竞争。一种可能的正确方法是也锁定读取。

回答您关于 volatile 是否解决了从 seq 读取被优化的问题:编译器不会从 seq 中删除两个读取,但这并没有解决任何问题,因为 seq 仍然容易出现数据竞争,也会导致未定义的行为。 volatile 不是这个意思,所以不要滥用它。

如前所述 - 您不应使用 volatile 进行线程间同步。这就是原因(来自 C++ 标准):

[..] volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation.[...]

volatile不做的是确保一个线程中的操作顺序(尤其是内存读取和写入)以相同的顺序可见在其他线程中 (due to superscalar architecture of modern CPUs) 。为此,您需要 memory barriersmemory fences(同一事物的不同名称)。以下是您可能会觉得有用的更多阅读材料:

答案吧:视情况而定。您是否有理由怀疑您的编译器不知道从回调函数中执行的代码可以随时执行?在托管系统编译器(Windows/Linux 等)上通常不是这种情况,但在嵌入式系统中很可能是这种情况,尤其是裸机或 RTOS。

这个话题有点像死马,比如here:

What volatile does:

  • Guarantees an up-to-date value in the variable, if the variable is modified from an external source (a hardware register, an interrupt, a different thread, a callback function etc).
  • Blocks all optimizations of read/write access to the variable.
  • Prevent dangerous optimization bugs that can happen to variables shared between several threads/interrupts/callback functions, when the compiler does not realize that the thread/interrupt/callback is called by the program. (This is particularly common among various questionable embedded system compilers, and when you get this bug it is very hard to track down.)

What volatile does not:

  • It does not guarantee atomic access or any form of thread-safety.
  • It cannot be used instead of a mutex/semaphore/guard/critical section. It cannot be used for thread synchronization.

What volatile may or may not do:

  • It may or may not be implemented by the compiler to provide a memory barrier, to protect against instruction cache/instruction pipe/instruction re-ordering issues in a multi-core environment. You should never assume that volatile does this for you, unless the compiler documentation explicitly states that it does.

不要使用 volatile,请使用 std::atomic<>volatile 旨在用于与内存映射硬件交互,std::atomic<> 旨在用于线程同步。使用正确的工具完成工作。

好的std::atomic<>实现的特点:

  • 它们对于标准整数类型是无锁的(通常是 long long 以内的所有类型)。

  • 它们适用于任何数据类型,但会对复杂数据类型使用透明互斥。

  • 如果std::atomic<>是无锁的,它会插入正确的内存barriers/fences以实现正确的语义。

  • std::atomic<>的操作无法优化掉,毕竟它们是为线程间通信设计的。