C++11引入了标准化的内存模型,但这到底意味着什么?它将如何影响C++编程?
这篇文章(加文·克拉克引用赫伯·萨特的话)说,
内存模型意味着C++代码现在有一个标准化的图书馆可以调用无论编译器是谁制造的以及在哪个平台上运行。有一种标准的方法来控制不同的线程与处理器的内存。“当你谈论分裂时[代码]跨越不同的核心在标准中,我们谈论的是记忆模型。我们将在不破坏以下假设萨特说。
嗯,我可以在网上记住这段和类似的段落(因为我从出生起就有自己的记忆模型:P),甚至可以发帖回答别人提出的问题,但老实说,我并不完全理解这一点。
C++程序员以前就开发过多线程应用程序,那么,是POSIX线程、Windows线程还是C++11线程又有什么关系呢?有什么好处?我想了解底层细节。
我还感觉到,C++11内存模型与C++11多线程支持有某种关系,正如我经常看到的那样。如果是,具体如何?为什么它们应该是相关的?
我不知道多线程的内部工作原理,也不知道内存模型的一般含义。
这是一个已有多年历史的问题,但非常受欢迎,值得一提的是,这是一份了解C++11内存模型的绝佳资源。我认为总结他的演讲是没有意义的,以便做出另一个完整的答案,但考虑到这是真正编写标准的人,我认为很值得观看演讲。
赫伯·萨特(Herb Sutter)就C++11内存模型进行了长达三个小时的演讲,题为“原子武器”(atomic<>Weapons),可在第9频道网站YouTube上获得-第1部分和第2部分。这场演讲技术性很强,涉及以下主题:
优化、竞赛和内存模型订购–内容:获取和发布订购–方式:互斥、原子和/或围栏对编译器和硬件的其他限制代码生成和性能:x86/x64、IA64、POWER、ARM放松原子学
演讲没有详细阐述API,而是讨论了推理、背景、幕后和幕后(你知道吗,放松的语义只是因为POWER和ARM不能有效地支持同步加载而被添加到标准中的吗?)。
上面的答案触及了C++内存模型的最基本方面。在实践中,std::atomic<>的大多数用法“只是起作用”,至少在程序员过度优化之前(例如,通过尝试放松太多东西)。
有一个地方错误仍然很常见:序列锁。在https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf.序列锁很有吸引力,因为读取器避免写入锁定字。以下代码基于上述技术报告的图1,它突出了在C++中实现序列锁时的挑战:
atomic<uint64_t> seq; // seqlock representation
int data1, data2; // this data will be protected by seq
T reader() {
int r1, r2;
unsigned seq0, seq1;
while (true) {
seq0 = seq;
r1 = data1; // INCORRECT! Data Race!
r2 = data2; // INCORRECT!
seq1 = seq;
// if the lock didn't change while I was reading, and
// the lock wasn't held while I was reading, then my
// reads should be valid
if (seq0 == seq1 && !(seq0 & 1))
break;
}
use(r1, r2);
}
void writer(int new_data1, int new_data2) {
unsigned seq0 = seq;
while (true) {
if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
break; // atomically moving the lock from even to odd is an acquire
}
data1 = new_data1;
data2 = new_data2;
seq = seq0 + 2; // release the lock by increasing its value to even
}
虽然最初看起来不直观,但data1和data2需要是原子的<>。如果它们不是原子的,那么可以在写入它们的同时(在reader()中)读取它们。根据C++内存模型,即使reader()从未实际使用数据,这也是一场竞赛。此外,如果它们不是原子的,那么编译器可以缓存寄存器中每个值的第一次读取。显然你不想这样。。。您希望在reader()中的while循环的每次迭代中重新读取。
使它们成为原子<>并使用memory_order_relaxed访问它们也是不够的。原因是seq的读取(在reader()中)只具有获取语义。简单地说,如果X和Y是内存访问,X在Y之前,X不是获取或释放,Y是获取,那么编译器可以在X之前对Y进行重新排序。如果Y是seq的第二次读取,而X是数据的读取,这样的重新排序会破坏锁的实现。
本文给出了一些解决方案。今天性能最好的可能是在第二次读取seqlock之前使用带有memory_order_relaxed的atomic_thread_fence。在本文中,它是图6。我没有在这里重复代码,因为任何读过这篇文章的人都应该读这篇文章。它比这篇文章更准确、更完整。
最后一个问题是,使数据变量原子化可能是不自然的。如果不能在代码中进行转换,则需要非常小心,因为从非原子转换为原子只对原始类型合法。C++20应该添加atomic_ref<>,这将使这个问题更容易解决。
总结一下:即使您认为自己理解C++内存模型,在滚动自己的序列锁之前也应该非常小心。