文章目录
- 一、C++ 内存模型
- 1、为什么需要内存模型?
- 2、happens-before和synchronize-with两个关键概念
- 2.1、happens-before
- 2.2、synchronize-with
- 2.3、总结
前言
C++ 11标准中最重要的特性之一,是大多数程序员都不会关注的东西。它并不是新的语法特性,也不是新的类库功能,而是新的多线程感知内存模型。本文介绍的内存模型是指多线程编程方面,而非对象的内存布局与内存对齐之类。
一、C++ 内存模型
1、为什么需要内存模型?
要回答这个问题需要从CPU架构说起,下面是一个的多核CPU架构图,如下:
- Core:每个Core独享SB与L1
- SB(Store Buffer):Store Buffer是一个缓冲区,用于暂存CPU的写操作,它允许CPU把数据先写入Store Buffer,然后继续执行其他操作,而不是等待数据被写入缓存或内存后再进行下一步。这样设计的好处是显著降低了内存写延迟对CPU性能的影响。
- L1 Cache:存储了CPU近期可能访问的数据和指令,且两个L1 Cache独享一个L2 Cache
注意:上面的CPU架构只是部分CPU采用的架构,并不能代表全部。
为什么需要使用Cache?
如果没有Cache,CPU每执行一条指令就要到内存中取数据。执行一条指令只需要几个时钟周期,而取指令需要上百个时钟周期,这就将导致CPU大部分时间都处于等待状态,进而导致执行效率低下,引入了Cache主要解决CPU等待问题。
使用Cache会引入一些新的问题,例如:Cache的一致性、Cache的缺失等,为了解决这些问题,各CPU平台(ARM/X86/IA64)都有自己的解决方案。软件层面(编译器)也会有对应的优化,这导致了CPU执行的程序并不是你写的那个版本,只是从结果上看不出差异而已。
编译器和CPU优化都遵循了一个同样的原则,即优化后的代码若是单线程执行,要与原有代码行为保持一致;再加之多线程环境我们使用的互斥锁,其对编译器与CPU优化做了很多限制,可以让我们对线程间的执行顺序进行同步,进而保证即使被优化成了另一个程序,仍然有相同的执行结果。
若是我们从比互斥锁更为底层地去了解线程间的同步机制,我们势必会看到CPU平台(ARM/X86)和编译器的差异,进而可以知道优化的存在。现代C++的内存模型,便是为了屏蔽这些差异,而让你可以不用去了解特定平台特定编译器,也不用依赖互斥锁,从更为底层的层面,就可以完成线程间的同步。C++ 11开始提供的
std::atomic
模版,便可以作为更为底层的同步工具,这也是内存模型起作用的地方。
2、happens-before和synchronize-with两个关键概念
在C++多线程编程中,确保不同线程之间的同步和数据一致性是至关重要的问题。为此,C++11及其后续版本引入了内存模型(Memory Model),其中包括了happens-before和synchronize-With两个关键概念。这两个概念帮助程序员理解多线程环境下操作的顺序和可见性,从而编写出正确且高效的并发代码。
2.1、happens-before
happens-before是C++内存模型中的一个核心概念,它定义了两个操作之间的偏序关系。如果一个操作A先行发生于另一个操作B(即A happens-before B),那么操作A的结果对操作B是可见的。这意味着操作B可以读取到操作A对共享数据的修改。在C++多线程环境中,happens-before关系可以通过多种方式建立,包括:
- 顺序一致性:在同一线程中,按照代码的执行顺序,前面的操作自然Happens-Before于后面的操作。
- 原子操作:原子操作(如
std::atomic
类型的操作)本身具有Happens-Before关系。对原子变量的写入操作Happens-Before于后续对同一变量的读取操作。 - 互斥锁:使用互斥锁(如
std::mutex
)保护的代码块之间也存在Happens-Before关系。解锁操作Happens-Before于随后的加锁操作,确保了临界区内的操作对其他线程可见。 - 条件变量:条件变量(如
std::condition_variable
)的等待和通知操作也遵循Happens-Before规则。通知操作Happens-Before于等待操作的返回,确保了通知状态的正确传播。
通过利用这些happens-before关系,程序员可以确保线程间的数据一致性和操作的顺序性。
2.2、synchronize-with
synchronize-with是C++多线程中另一个重要的概念,它关注于特定同步操作之间的关系。当两个线程通过某种同步机制(如互斥锁、条件变量或原子操作)进行交互时,它们之间的操作会形成synchronize-with关系。
具体来说,如果一个线程A释放了一个锁或修改了一个原子变量,并且随后线程B获取了同一个锁或读取了同一个原子变量,那么线程A的操作synchronize-with线程B的操作。这意味着线程A中所有对共享数据的修改对线程B都是可见的。
synchronize-with关系提供了一种更细粒度的控制,它允许程序员精确地指定哪些操作需要在多线程环境中保持同步和可见。通过合理地使用同步机制,程序员可以确保线程之间的正确交互和数据一致性。
2.3、总结
在C++多线程编程中,happens-before和synchronize-with是两个核心概念,它们共同构成了C++内存模型的基础。通过理解和利用这些概念,程序员可以编写出高效且正确的并发代码,确保线程间的同步和数据一致性。在实际应用中,程序员应该根据具体的需求和场景选择合适的同步机制,并遵循内存模型的规则,以确保多线程程序的正确性和性能。