文章目录
代码:https://github.com/leoda1/the-notes-of-cuda-programming/tree/main/code/CPU
1. 原子变量(Atomic Variable)
1.1 定义
c++11中提供的原子类型std::atomic<T>,内部变量通过这个原子类型管理后就变为原子变量。原子类型模板参数<T>可以指定bool、char、int、long、指针等类型(不支持浮点类型和复合类型)。
原子指一系列不可被CPU上下文交换的机器指令,这些指令组合在一起就形成了原子操作。在多核CPU下,当某个CPU核心开始运行原子操作时,会先暂停其它CPU内核对内存的操作,以保证原子操作不会被其它CPU内核所干扰。
由于原子操作是通过指令提供的支持,因此它的性能相比锁和消息传递会好很多。相比较于锁而言,原子类型不需要开发者处理加锁和释放锁的问题,同时支持修改,读取等操作,还具备较高的并发性能,几乎所有的语言都支持原子类型。
可以看出原子类型是无锁类型,但是无锁不代表无需等待,因为原子类型内部使用了CAS循环,当大量的冲突发生时,该等待还是得等待!但是总归比锁要好。
c++11内置了int形的原子变量,用于更方便的使用它。多线程操作中,原子变量使用后不需要使用互斥量保护该变量。因为对原子变量的操作只能是原子操作(不会被线程调度机制打断的操作,一旦开始就一直运行到结束,中间没有上下文切换)。同时,多线程访问的共享资源造成的数据混乱(比如一个线程在修改这个数据,另一个线程也在修改相同的数据),此时使用原子变量可以很好的解决。通过使用原子操作指令,如lock cmpxchg,fetch_add等来确保数据操作不可被其他线程打断。例如:
// Atomic_Op_exp1.cpp
#include <iostream>
#include <thread>
using namespace std;
int main () {
int sum = 0;
auto f = [&sum] () {
for (int i = 0; i < 12345678; i ++) {
sum += 1;
}
};
thread td1(f);
thread td2(f);
td1.join();
td2.join();
cout << "sum: " << sum << endl;
return 0;
}
/******************************************************************
sum: 16333875 test 1
sum: 12906647 test 2
sum: 14225737 test 3
*******************************************************************/
这个代码当中存在td1和td2两个线程执行任务f,任务f中的lamba表达式是对sum做累加,理论上得到的结果应该是2 * 12345678,但是每次都不对,就是因为多个线程同时对 sum 进行读取、修改、写入的操作,会引发数据竞争(data race),导致结果不确定。
在多线程环境下,如果两个线程同时进行上述操作,就可能出现以下情况:
- 线程 A 读取
sum的值为 12345678。 - 线程 B 读取
sum的值也为 12345678。 - 线程 A 将值加 1 并写回,
sum的值变为 12345679。 - 线程 B 将值加 1 并写回,
sum的值也变为 12345679。
这样,两个线程各执行了一次加 1 操作,但 sum 的值只增加了 1。这就是数据竞争导致的错误。
1.2 atomic的缓存机制
atomic变量不会存储到缓存中而是运算完后直接给内存,所以先谈一下缓存机制:
| 普通变量的缓存机制 | atomic变量的缓存机制 |
|---|---|
| 1. 先把数据加载到CPU缓存(L1/L2/L3 cache) 2. 在cache中修改数据,合适时间再 写回主内存 | 变量在修改后,会直接让 所有 CPU 核心立即看到最新的值,不会延迟同步。 |

所以,当涉及到复合类型的数据(class 或者 struct)的时候,atomic无能为力。复杂数据结构的修改通常需要多个步骤(如修改多个字段),而 std::atomic 只能保证单个变量的 不可中断性。
atomic 指针本身的修改(++/--)是原子的,但指针指向的对象并不是原子的,需要额外的同步机制(如 mutex 或 std::shared_ptr)。
1.3 atomic类
atomic类定义:
// 定义于头文件 <atomic>
template< class T > // so, 在使用这个模板类的时候,一定要指定模板类型。
struct atomic;
atomic的构造函数是:
// 1 默认无参构造函数。
atomic() noexcept = default;
// 2 使用 desired 初始化原子变量的值。
constexpr atomic( T desired ) noexcept;
// 3 使用=delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
atomic( const atomic& ) = delete;
// exp1
std::atomic<int> x;
x.store(10);
// exp2
std::atomic<int> x(5);
// exp3
std::atomic<int>& x
atomic的公共成员函数:
原子类型在类内部重载了=操作符,并且不允许在类的外部使用 =进行对象的拷贝。
T operator=( T desired ) noexcept;
T operator=( T desired ) volatile noexcept;
atomic& operator=( const atomic& ) = delete;
atomic& operator=( const atomic& ) volatile = delete;
以原子操作的方式,将 desired 作为新值存储到原子变量中,并按照 order 指定的内存顺序来影响内存操作。
void store( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept;
void store( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept;
原子地加载并返回原子变量的当前值。按照 order 的值影响内存。直接访问原子对象也可以得到原子变量的当前值。
T load( std::memory_order order = std::memory_order_seq_cst ) const noexcept;
T load( std::memory_order order = std::memory_order_seq_cst ) const volatile noexcept;
范例:
// Atomic_Op_exp2.cpp
void test01()
{
atomic_char cc('b');
cc = 'd';
cout << cc << endl;
cc.store('a');
cout << cc << endl;
char ccc = cc.exchange('e');//返回之前的旧值
cout << cc.load() << endl;
cout << ccc << endl;
}
/******************************************************************
d
a
e
a
*******************************************************************/
1.4 特化成员函数(特化:为某些特定类型提供不同的实现)
对于整数类型的 std::atomic<T>,以下运算符提供 原子修改 变量的功能:
| 操作符 | 重载函数 | 等价的 fetch_* 方法 | 适用于整数类型 | 适用于指针 |
|---|---|---|---|---|
+= | atomic::operator+= | atomic::fetch_add | ✅ 是 | ✅ 是 |
-= | atomic::operator-= | atomic::fetch_sub | ✅ 是 | ✅ 是 |
&= | atomic::operator&= | atomic::fetch_and | ✅ 是 | ❌ 否 |
^= | atomic::operator^= | atomic::fetch_xor | ✅ 是 | ❌ 否 |
对于指针类型 std::atomic<T*>,仅支持 += 和 -= 操作,用于 原子地移动指针:
| 操作符 | 重载函数 | 等价的 fetch_* 方法 | 适用于整数类型 | 适用于指针 |
|---|---|---|---|---|
+= | atomic::operator+= | atomic::fetch_add | ✅ 是 | ✅ 是 |
-= | atomic::operator-= | atomic::fetch_sub | ✅ 是 | ✅ 是 |
operator+= (ptrdiff_t val): 指针向前移动val个元素。operator-= (ptrdiff_t val): 指针向后移动val个元素。
2 CAS操作(compare and swap,比较并交换)
CAS(Compare And Swap,比较并交换) 是 无锁编程(Lock-Free Programming) 中的一种 原子操作,用于 实现多线程安全的数据更新。它允许多个线程 安全地竞争更新同一个变量,而不需要使用互斥锁(mutex)。
没有交换操作,CAS 就不完整。交换操作可通过 exchange() 成员函数实现(前面的Atomic_Op_exp2.cpp中已经用过了)。调用 exchange() 会将原子的当前值与所需值互换,并返回原来的值。所有操作均以原子方式完成。
CAS 主要由三个值组成:
- 原子变量(x):要修改的变量,通常是
std::atomic<T>类型。 - 预期值(expected):期望
x当前的值。 - 目标值(desired):希望
x更新成的新值。
工作原理
- 检查
x是否等于expected(即该变量在读取之后是否仍未被其他线程修改)。 - 如果相等,说明没有其他线程修改
x,那么就 将x更新为desired,并返回true。 - 如果不相等,说明
x在此期间被其他线程修改过,此时 将expected更新为x的最新值,并返回false,通常需要重新尝试(重试循环)。
CAS 在swap前增加了一个附加条件,可通过 compare_exchange_strong() 和 compare_exchange_weak()这两个函数使用。它将一个期望值与原子变量进行比较。如果它们相等,原子变量将被交换,并返回 true。否则,原子变量的当前值将被加载到expected变量,并返回 false。这种机制允许我们创建一个重试循环,而无需再次显式地读取原子值。例如:
std::atomic<int> x(0);
int expected = x.load();
int desired = 42;
// If x == expected, x = desired and return true
// Otherwise, expected = x and return false
while(!x.compare_exchange_strong(expected, desired));
CAS 会失败的原因是与其他线程的争用。在我们对原子进行初始读取以获得预期值后,我们必须确保其他线程在我们上次读取原子后没有对其进行更改。假设多个线程都在尝试 CAS,那么重试循环就会一直运行,直到我们的 CAS 比其他线程更快地完成更改。
2.1 在需要严格保证操作成功或失败时使用 compare_exchange_strong
// Atomic_Op_exp3.cpp
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> atomicInt(0);
void updateValue() {
int expected = 0;
int desired = 1;
if (atomicInt.compare_exchange_strong(expected, desired)) {
cout << "Value changed to " << desired << endl;
} else {
cout << "Expected " << expected << " but found " << atomicInt.load() << endl;
}
}
int main() {
thread t(updateValue);
t.join();
return 0;
}
/******************************************************************
Value changed to 1
*******************************************************************/
2.2 使用 compare_exchange_weak 的场景
compare_exchange_weak() 可能在某些 CPU 架构(如 ARM)在 LL/SC 机制下,出现伪失败。即使当前值等于预期值,也返回false。设计出来是为了优化在某些架构上的性能,适用于需要反复尝试的场景,如实现自旋锁或无锁数据结构。
示例1:自旋锁的代码实现:
// Atomic_Op_exp5.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
int counter;
atomic<bool> lockFlag(false);
void spinlock_get() {
bool expected = false;
while (!lockFlag.compare_exchange_weak(expected, true)) {
expected = false;
}
}
void spinlock_free() {
lockFlag.store(false);
}
void increment() {
for (int i = 0; i < 123456; i ++) {
spinlock_get();
++counter;
spinlock_free();
}
}
int main () {
const int numThreads = 100;
vector<thread> threads;
for (int i = 0; i < numThreads; i ++) {
threads.push_back(thread(increment));
}
for (auto& t : threads) {
t.join();
}
cout << "Final counter value: " << counter << endl;
return 0;
}
/******************************************************************
Final counter value: 12345600
//如果没有用自旋锁 输出的是:Final counter value: 7059450
*******************************************************************/
这段代码的lockFlag 作为 自旋锁(spinlock),初始值为 false(表示没有线程持有锁)。10个线程完成increment函数(累加1000次1),这个函数每次累加的时候会获取自旋锁和释放自旋锁。
自旋锁的实现:如果 lockFlag == expected(false),说明 锁是空闲的,于是将 lockFlag 设为 true(成功加锁)。如果 lockFlag != expected(说明其他线程已经持有锁),CAS 失败,返回 false,并 更新 expected = lockFlag(但这里 expected 直接重新设为 false,以便下一次尝试)。不断循环,直到成功获取锁。
示例2:无锁队列的实现
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
class Node {
public:
int value;
Node* next;
Node(int val) : value(val), next(nullptr) {}
};
std::atomic<Node*> head(nullptr);
void push(int value) {
Node* newNode = new Node(value);
Node* oldHead;
do {
oldHead = head.load(std::memory_order_relaxed);
newNode->next = oldHead;
} while (!head.compare_exchange_weak(oldHead, newNode, std::memory_order_release, std::memory_order_relaxed));
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(push, i));
}
for (auto& t : threads) {
t.join();
}
Node* current = head.load();
while (current) {
std::cout << current->value << " ";
Node* tmp = current;
current = current->next;
delete tmp;
}
std::cout << std::endl;
return 0;
}
2.3 Strong vs Weak
compare_exchange_strong() 只有在原子的当前值由于争用而不再是我们所期望的值时,才会失败。
另一方面,compare_exchange_weak()允许虚假失败。这意味着,即使预期值等于当前值,它仍可能返回 false。
但为什么我们希望即使当前值等于预期值,CAS 也会失败呢?这是因为在某些平台上,获取独占访问可能代价非常大,但读取原子值却往往代价很小。
因此,弱 CAS 不会要求独占访问,而是执行一定时间的尝试来获取锁(硬件),这可能会超时–类似于网络套接字。
| 步骤 | 强 (compare_exchange_strong) | 弱 (compare_exchange_weak) |
|---|---|---|
| 1. 线程读取原子变量并将其与预期值进行比较。 | 如果匹配,进行强制尝试以获得独占访问,即等待直到成功。 如果不匹配,将当前值存储在预期变量中,并立即返回 false。 | 如果匹配,它将进行一次有时限的尝试以获得独占访问。如果不匹配,将当前值存储在预期变量中,并立即返回 false。 |
| 2. 获取独占访问 | 等待直到我们获得独占访问。 | 如果我们的有时限的尝试超时,我们返回 false——即使预期值与当前值匹配。 |
| 3. 已获得独占访问 | 一旦获得独占访问,我们再次将当前值与预期值进行比较,以防它发生了变化。如果匹配,执行交换并返回 true。否则,将当前值存储在预期变量中,并返回 false。 | 一旦获得独占访问,我们再次将当前值与预期值进行比较,以防它发生了变化。如果匹配,执行交换并返回 true。否则,将当前值存储在预期变量中,并返回 false。 |
3 内存顺序
编译器和 CPU 能够对程序指令进行重新排序,通常彼此独立。也就是说,编译器可以重新排序指令,CPU 可以再次重新排序指令。但是,只有当编译器在两组指令之间没有建立任何依赖关系时,才允许这样做。
例如,下面的代码可以重新排序,因为对 x 的赋值和对 y 的赋值之间没有关系。也就是说,编译器或 CPU 可能先分配 y,然后分配 x。但是,这不会改变程序的语义含义。
int x = 10;
int y = 5;
另一方面,下面的代码示例不能重新排序,因为编译器建立了 x 和 y 之间的关系。这里很容易看出,因为 y 取决于 x 的值。
int x = 10;
int y = x + 1;
我们先从一个简单的例子开始,一步步的引出内存顺序的概念:
/******************************************************************
* Author : Da Liu
* Date : 2025-03-07
* File Name : Atomic_Op_exp6.cpp
* Description : 内存顺序(注释掉的代码取消注释就可以实现正确的数据
* 访问了,当前的代码会出现assert)
*****************************************************************/
#include <cassert>
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
int data_test = 0;
// atomic<bool> ready(false);
void product() {
data_test = 100;
// ready.store(true); // Set flag
}
void consumer() {
// while (!ready.load()) {
// this_thread::sleep_for(chrono::milliseconds(1));
// }
assert(data_test == 100);
}
int main() {
thread t1(product);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
/******************************************************************
Atomic_Op_exp6: Atomic_Op_exp6.cpp:19: void consumer(): Assertion `
data_test == 100' failed.
Aborted (core dumped)
*******************************************************************/
这里的CPU 或编译器可能会对 (1) 和 (2) 进行重排序,导致:
store(ready, true) // (2) 先执行
write(data_test , 100) // (1) 后执行
那么,线程 2 可能看到:
while (!ready.load()); // (3) 读取到 true,退出循环
std::cout << data; // (4) 读取 data,此时 data 可能仍然是 0!
导致 线程 2 读取到了错误的数据(脏读)。
内存屏障(Memory Barrier) 是 一条特殊的 CPU 指令,它告诉 CPU 在它之前的所有内存操作必须执行完成后,才能执行它之后的内存操作,从而防止指令重排序。
修改代码为:
ready.store(true, std::memory_order_seq_cst); //product函数内的
while (!ready.load(std::memory_order_seq_cst)); //consumer函数内的
memory_order_seq_cst是 最强的同步顺序,它确保:- 所有线程 都以 相同的全局顺序 观察所有
seq_cst操作。 - 不能被重排序,确保 (1)
data = 100;一定会在 (2)ready.store(true);之前执行。
- 所有线程 都以 相同的全局顺序 观察所有
- 内存屏障 保证:
- 线程 1:
data = 100;必须在ready.store(true);之前完成。 - 线程 2:
ready.load(true);一旦读取为true,就保证它能看到data = 100。
- 线程 1:
3.1 内存顺序的类型
现在总结上面的如下:

| Memory Order | 是否禁止重排序? | 是否保证跨线程可见性? | 适用场景 |
|---|---|---|---|
memory_order_relaxed | ❌ 可能重排序 | ❌ 不保证跨线程可见性 | 适用于 无数据依赖 的原子计数、自增等操作 |
memory_order_acquire | ✅ 禁止后面的指令重排序 | ✅ 能看到 memory_order_release 之前的修改 | 适用于 消费者(读取者)同步,如 load() |
memory_order_release | ✅ 禁止前面的指令重排序 | ✅ 确保修改能被 memory_order_acquire 线程看到 | 适用于 生产者(写入者)同步,如 store() |
memory_order_acq_rel | ✅ 禁止前后指令的重排序 | ✅ 用于同步 load() 和 store() 操作 | 适用于 读-改-写 原子操作,如 fetch_add() |
memory_order_seq_cst | ✅ 完全禁止重排序 | ✅ 所有线程按相同顺序观察所有操作 | 最严格的同步,适用于 线程间通信 |
3.2 原子变量和std::mutex之间的效率对比
#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
const int NUM_THREADS = 8;
const int NUM_ITERATIONS = 1000000;
void atomicIncrement(std::atomic<int>& counter) {
for (int i = 0; i < NUM_ITERATIONS; ++i) {
++counter;
}
}
void mutexIncrement(int& counter, std::mutex& mtx) {
for (int i = 0; i < NUM_ITERATIONS; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
// 使用原子变量
std::atomic<int> atomicCounter(0);
auto atomicStart = std::chrono::high_resolution_clock::now();
std::vector<std::thread> atomicThreads;
for (int i = 0; i < NUM_THREADS; ++i) {
atomicThreads.emplace_back(atomicIncrement, std::ref(atomicCounter));
}
for (auto& t : atomicThreads) {
t.join();
}
auto atomicEnd = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> atomicDuration = atomicEnd - atomicStart;
std::cout << "Atomic counter final value: " << atomicCounter << std::endl;
std::cout << "Atomic duration: " << atomicDuration.count() << " seconds" << std::endl;
// 使用互斥锁
int mutexCounter = 0;
std::mutex mtx;
auto mutexStart = std::chrono::high_resolution_clock::now();
std::vector<std::thread> mutexThreads;
for (int i = 0; i < NUM_THREADS; ++i) {
mutexThreads.emplace_back(mutexIncrement, std::ref(mutexCounter), std::ref(mtx));
}
for (auto& t : mutexThreads) {
t.join();
}
auto mutexEnd = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> mutexDuration = mutexEnd - mutexStart;
std::cout << "Mutex counter final value: " << mutexCounter << std::endl;
std::cout << "Mutex duration: " << mutexDuration.count() << " seconds" << std::endl;
return 0;
}

1万+

被折叠的 条评论
为什么被折叠?



