我正在实施一个序列锁定' class允许锁定写入和无锁读取数据结构。
包含数据的结构包含序列值,在写入发生时,该值将递增两次。在写作开始之前,写作完成之后一次。作者在读者之外的其他线程上。
这是包含数据副本的结构,序列值如下所示:
template<typename T>
struct seq_data_t
{
seq_data_t() : seq(0) {};
int seq; <- should this be 'volatile int seq;'?
T data;
};
整个序列锁定类在循环缓冲区中保存此结构的N个副本。编写器线程总是在循环缓冲区中写入最旧的数据副本,然后将其标记为当前副本。写作是互斥锁定的。
读取功能无法锁定。它试图阅读“当前&#39;数据副本。它存储了&#39; seq&#39;阅读前的价值。然后它读取数据。然后它再次读取seq值,并将其与第一次读取的值进行比较。如果seq值没有改变,则认为读数良好。
由于作者线程可以改变&#39; seq&#39;当读取正在发生时,我认为seq变量应该标记为volatile,以便read函数在读取数据后显式读取该值。
read函数如下所示:它将在除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)在具有多个成员的struct的上下文中,只有volatile的合格变量,而不是其他成员?即只是&#39; seq&#39;如果我只在结构中标记它是volatile吗?
答案 0 :(得分:5)
请勿使用volatile
,请使用std::atomic<>
。 volatile
旨在用于与内存映射硬件交互,std::atomic<>
旨在用于线程同步。使用正确的工具完成工作。
良好std::atomic<>
实施的功能:
对于标准整数类型(通常最多long long
),它们无锁。
它们适用于任何数据类型,但会对复杂数据类型使用透明互斥。
如果std::atomic<>
无锁,则会插入正确的内存屏障/栅栏以实现正确的语义。
std::atomic<>
的操作无法优化,毕竟它们是为线程间通信而设计的。
答案 1 :(得分:2)
如上所述Is volatile required here - 你不要使用volatile
进行线程间同步。这就是为什么(来自C ++标准):
[..] volatile是一个提示,以避免攻击性 涉及对象的优化因为对象的值 可能会被实现无法察觉的方式改变。[...]
volatile
无法做什么确保一个线程中的操作序列(尤其是内存读写)在其他线程中以相同的顺序可见(due to superscalar architecture of modern CPUs)。为此,您需要内存屏障或内存屏障(同一事物的不同名称)。以下是一些您可能会觉得有用的阅读材料:
答案 2 :(得分:1)
1)在这种情况下,seq必须是易变的吗?
当然,很可能seq
的阅读将使用-O3
进行优化。所以,是的,您应该提示编译器seq
可能在其他位置(即在其他线程中)使用volatile
关键字进行更改。
对于x86架构来说就足够了,因为x86内存模型(几乎)按照on Wikipedia描述的顺序排列。
为了便于携带,最好使用原子基元。
2)在具有多个成员的struct的上下文中,只有volatile的合格变量,而不是其他成员?即如果我只在结构中将其标记为易失性,那么它只是'seq'易失性吗?
不,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&lt;&gt; Weapons”,它解释了编译器和其他内存障碍以及原子:https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2
答案 3 :(得分:1)
如果代码是可移植的,除非处理内存映射硬件,否则volatile
永远不合适。我再说一遍,从不合适。 Microsoft Visual C ++,(x86或x86 / 64),使用默认编译器标志,添加了一些不在标准中的内存顺序保证。因此,使用该编译器,在启用非标准行为的情况下,volatile
可能适用于某些多线程操作。
使用标准的多线程支持,例如std :: atomic,std :: mutex,std :: condition_variable等。
答案 4 :(得分:-1)
实际问题是,在写入时从某些内存(在这种情况下为data
)读取被描述为数据争用,因此程序的行为未定义。即使您使seq
为原子,从data
读取仍会导致数据竞争。一种可能的正确方法是锁定读取。
回答您关于volatile
是否解决了seq
被优化的读取的问题:编译器不会从seq
删除这两个读取,但这并不是解决任何问题,因为seq
仍然容易发生数据争用,导致未定义的行为。这不是volatile
的意思,所以不要滥用它。
答案 5 :(得分:-1)
答案:这取决于。您是否有理由怀疑您的编译器不知道在回调函数中执行的代码可以随时执行?在托管系统编译器(Windows / Linux等)上通常不是这种情况,但在嵌入式系统中尤其如此,特别是裸机或RTOS。
这个主题是一种挨打的死马,例如here:
挥发性有什么作用:
- 如果变量是从外部源(硬件寄存器,中断,不同的线程,回调函数等)修改的,则保证变量中的最新值。
- 阻止对变量进行读/写访问的所有优化。
- 当编译器没有意识到程序调用线程/中断/回调时,防止在多个线程/中断/回调函数之间共享的变量可能发生的危险优化错误。 (这在各种有问题的嵌入式系统编译器中尤为常见,当你遇到这个错误时,很难找到它。)
什么挥发性不会:
- 它不保证原子访问或任何形式的线程安全。
- 不能使用它来代替互斥锁/信号量/警卫/关键部分。它不能用于线程同步。
什么挥发性可能会或可能不会:
- 编译器可能会或可能不会实现提供内存屏障,以防止多核环境中的指令缓存/指令管道/指令重新排序问题。你永远不应该假设volatile会为你做这件事,除非编译器文档明确指出它。