- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
搞懂无锁编程的重要一步是完全理解内存顺序! 。
本教程由作者和ChatGPT通力合作完成.
c++的内存模型共有6种 。
如果你有幸阅读过cpprefence的这一章节,我想你一定会对这些概念的晦涩难懂有深刻的印象!有大量的教程都会从 memory_order_relaxed 的概念开始介绍,我认为这是不妥的,如果对内存顺序没有一个大致的了解之前,没有对比,你根本无法得知“宽松”到底意味着什么,到底宽松在什么地方.
因此,我觉得有必要先对 memory_order_acquire 和 memory_order_release 进行了解。我们需要指导 memory_order_acquire 和 memory_order_release 会对我们的代码到底产生怎样的影响,需要理解他们为什么这样命名.
内存顺序 memory_order_acquire 表示该操作需要在后续的读操作中确保获得在当前操作之前已经完成的所有写操作的结果。即,在当前操作之前,所有的写操作必须在内存中完成,然后该操作才能进行。这意味着 memory_order_acquire 会阻止处理器和编译器在该操作和后续读操作之间重新排序.
内存顺序 memory_order_release 表示该操作需要在当前操作之前确保所有已经完成的读和写操作都要被立即刷新到内存中。也就是说,该操作会将其前面的写操作立即刷新到内存中,这样后续的读操作就能够访问到这些数据了。同时,该操作之后的写操作也不能被重排序到该操作之前.
以上是ChatGPT的解释,不是我的解释,仅仅依靠文本解释,往往会让人一头雾水。如果用来解释概念的概念仍然是你不懂得概念,那么这个解释本身就成为了学习的门槛,因此,我们必须要抽丝剥茧,慢慢来.
在上述解释中,我们注意到,这里有一个关键概念: 指令重排.
是的,无论是否在多线程环境下,由于编译器对代码的优化,实际的汇编指令的顺序,有可能与c++代码的顺序不一致(处理器也会对指令进行重排).
这种重排可以分为三种类型:
指令重排其实是一种优化手段,但它的出现也为多线程编程带来了麻烦,下面的例子展示了指令重排是如何影响多线程编程的:
int x = 0;
int y = 0;
void thread1() {
y = 2;
}
void thread2() {
while (y != 2) {}
assert(x == 1);
}
如果此时运行两个线程,我们期望的事情是,线程2一直等到 y == 2 ,然后检查 x 是否为1,当 y 已经等于2时,线程1执行了 x = 1;y = 2,
因此 x 一定等于1.
但由于指令重排,线程1的执行顺序有可能是 。
y = 2;
x = 1;
如果线程1刚刚执行完 y=2 ,线程2就开始执行,此时循环条件失败,断言语句在 x=1 前执行了,那么此时就会断言失败.
体会到指令重排给我们带来的麻烦了吗?
且看我们如何使用内存顺序来避免这种情况 。
int x = 0;
std::atomic_int y(0);
void thread1() {
x = 1;
y.store(2, std::memory_order_release);
}
void thread2() {
int tmp;
do {
tmp = y.load(std::memory_order_acquire);
} while (tmp != 2);
assert(x == 1);
}
在这个例子中memory_order_release memory_order_acquire 起到了什么作用呢?他是如何帮助我们解决重排问题的?让我们一步步解释
在这个例子中,使用了 memory_order_release 和 memory_order_acquire 两个内存顺序模型.
在解释这两个内存顺序模型之前,有必要介绍两个原子操作:
y.store(2, std::memory_order_release) 的意思是将2原子的写入y中,并使用 memory_order_release 要求内存顺序.
tmp = y.load(std::memory_order_acquire) 的意思从y中读取值并赋值给tmp,并使用 memory_order_acquire 内存顺序.
单独的原子操作只能影响单个线程,无论它携带怎样的内存顺序模型,仅仅使用对单个线程的某个原子操作使用顺序模型一般来说是没有任何意义的,就拿这个示例来说,线程1的写操作的memory_order_release表现用于保证在这个原子操作后,x=1必定是生效的,且在线程2的读操作使用memory_order_acquire内存顺序模型读取到的数值一定是线程1存储后的数值。这对原子操作本身和x都是一样的.
为了更加清楚的表达它们的概念,体会它们在多线程编程中的协作,我将会给出另一个示例,并携带注释,注释按照[1] [2] [3]的循序观看,请仔细阅读,确保已经完全理解:
#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string *> ptr;
int data;
void producer() {
auto *p = new std::string("Hello");
data = 42;
//store是一个写操作,std::memory_order_release要求在这个写指令完成后,所有的写操作必须也是完成状态,也就是说,
//编译器看到这条指令后,就不能将data = 42这条指令移动到store操作之后,这便是memory_order_release对内存顺序的要求
//memory_order_release中的release指的是将修改后(写操作)的结果释放出来,一旦其他线程使用了 memory_order_acquire,就可以观测到
//上述对内存写入的结果(release也可理解为释放内存的控制权)
ptr.store(p, std::memory_order_release);//[1]
}
void consumer() {
std::string *p2;
//此处等待ptr的store操作 while保证了时间上的同步,也就是会等到ptr写入的那一刻,memory_order_release,memory_order_acquire保证
//内存上的同步,也就是producer写入的值一定会被consumer读取到
while (!(p2 = ptr.load(std::memory_order_acquire)))//[3]
;
//如果执行到此处,说明p2是非空的,也就意味着ptr load 到了一个非空的字符串,也就意味着 data = 42的指令已经执行了(memory_order_release保证),
//且此时data必定等于42 ,p2必定为“Hello”(memory_order_acquire保证)
assert(*p2 == "Hello");// 绝无问题//[2]
assert(data == 42); // 绝无问题
}
int main() {
std::thread t2(consumer);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::thread t1(producer);
t1.join();
t2.join();
}
这些协作方式当然不是我杜撰的,而是在cppreference中有详细的解释,经过上述的例子,我们可以在从头看看文档中的内容了,请注意本章节存在大量摘抄文本,但是我仍然希望你能够在理解上一章节的基础上仔细阅读这些文本,文档是对概念最准确的解释,是必须要跨越的一关:
原文: std::memory_order - cppreference.com 。
首先让我们看看文档是如何定义这些内存操作的:
值 | 解释 |
---|---|
memory_order_relaxed | 宽松操作:没有同步或顺序制约,仅对此操作要求原子性(见下方 宽松顺序)。 |
memory_order_consume | 有此内存顺序的加载操作,在其影响的内存位置进行_消费操作_:当前线程中依赖于当前加载的该值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化(见下方 释放消费顺序)。 |
memory_order_acquire | 有此内存顺序的加载操作,在其影响的内存位置进行_获得操作_:当前线程中读或写不能被重排到此加载前。其他释放同一原子变量的线程的所有写入,能为当前线程所见(见下方释放获得顺序)。 |
memory_order_release | 有此内存顺序的存储操作进行_释放操作_:当前线程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得该同一原子变量的其他线程 释放获得顺序),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(见下方 释放消费顺序)。 |
memory_order_acq_rel | 带此内存顺序的读修改写操作既是_获得操作_又是_释放操作_。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。 |
memory_order_seq_cst | 有此内存顺序的加载操作进行_获得操作_,存储操作进行_释放操作_,而读修改写操作进行_获得操作_和_释放操作_,再加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(见下方序列一致顺序)。 |
接下来是他们之间的协作关系,这部分我会以自己的理解阐述它们,如果对原文感兴趣请点击上方链接:
与其他线程没有协作,仅仅保证原子性,也就是允许指令重排,仅仅保证这个原子变量的原子性.
释放获得顺序就是我上面给的例子那样,它规定了原子操作store with memory_order_release 时,在此代码行上方的内存读写操作都必须到位,而 load with memory_order_acquire 是一定可以取到 memory_order_release 所约束的那些变量的所写入的值.
释放消费顺比释放获得顺序要更加宽松,仅仅同步了原子操作本身的原子变量以及产生依赖关系的变量.
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume)))
;
assert(*p2 == "Hello"); // 绝无出错: *p2 从 ptr 携带依赖
assert(data == 42); // 可能也可能不会出错: data 不从 ptr 携带依赖
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
此代码不能保证data在线程中同步.
简单来说就是拒绝一切重排,对所有线程可见,而获得释放操作只能影响相关线程.
最后此篇关于c++内存顺序的文章就讲到这里了,如果你想了解更多关于c++内存顺序的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
#include using namespace std; class C{ private: int value; public: C(){ value = 0;
这个问题已经有答案了: What is the difference between char a[] = ?string?; and char *p = ?string?;? (8 个回答) 已关闭
关闭。此题需要details or clarity 。目前不接受答案。 想要改进这个问题吗?通过 editing this post 添加详细信息并澄清问题. 已关闭 7 年前。 此帖子已于 8 个月
除了调试之外,是否有任何针对 c、c++ 或 c# 的测试工具,其工作原理类似于将独立函数复制粘贴到某个文本框,然后在其他文本框中输入参数? 最佳答案 也许您会考虑单元测试。我推荐你谷歌测试和谷歌模拟
我想在第二台显示器中移动一个窗口 (HWND)。问题是我尝试了很多方法,例如将分辨率加倍或输入负值,但它永远无法将窗口放在我的第二台显示器上。 关于如何在 C/C++/c# 中执行此操作的任何线索 最
我正在寻找 C/C++/C## 中不同类型 DES 的现有实现。我的运行平台是Windows XP/Vista/7。 我正在尝试编写一个 C# 程序,它将使用 DES 算法进行加密和解密。我需要一些实
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
有没有办法强制将另一个 窗口置于顶部? 不是应用程序的窗口,而是另一个已经在系统上运行的窗口。 (Windows, C/C++/C#) 最佳答案 SetWindowPos(that_window_ha
假设您可以在 C/C++ 或 Csharp 之间做出选择,并且您打算在 Windows 和 Linux 服务器上运行同一服务器的多个实例,那么构建套接字服务器应用程序的最明智选择是什么? 最佳答案 如
你们能告诉我它们之间的区别吗? 顺便问一下,有什么叫C++库或C库的吗? 最佳答案 C++ 标准库 和 C 标准库 是 C++ 和 C 标准定义的库,提供给 C++ 和 C 程序使用。那是那些词的共同
下面的测试代码,我将输出信息放在注释中。我使用的是 gcc 4.8.5 和 Centos 7.2。 #include #include class C { public:
很难说出这里问的是什么。这个问题是含糊的、模糊的、不完整的、过于宽泛的或修辞性的,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开它,visit the help center 。 已关
我的客户将使用名为 annoucement 的结构/类与客户通信。我想我会用 C++ 编写服务器。会有很多不同的类继承annoucement。我的问题是通过网络将这些类发送给客户端 我想也许我应该使用
我在 C# 中有以下函数: public Matrix ConcatDescriptors(IList> descriptors) { int cols = descriptors[0].Co
我有一个项目要编写一个函数来对某些数据执行某些操作。我可以用 C/C++ 编写代码,但我不想与雇主共享该函数的代码。相反,我只想让他有权在他自己的代码中调用该函数。是否可以?我想到了这两种方法 - 在
我使用的是编写糟糕的第 3 方 (C/C++) Api。我从托管代码(C++/CLI)中使用它。有时会出现“访问冲突错误”。这使整个应用程序崩溃。我知道我无法处理这些错误[如果指针访问非法内存位置等,
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 7 年前。
已关闭。此问题不符合Stack Overflow guidelines 。目前不接受答案。 要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于 Stack Overflow 来说是偏离主题的,因为
我有一些 C 代码,将使用 P/Invoke 从 C# 调用。我正在尝试为这个 C 函数定义一个 C# 等效项。 SomeData* DoSomething(); struct SomeData {
这个问题已经有答案了: Why are these constructs using pre and post-increment undefined behavior? (14 个回答) 已关闭 6
我是一名优秀的程序员,十分优秀!