C++中std::atomic通过硬件指令实现共享变量的原子操作,避免数据竞争。它比互斥锁更轻量,适用于单变量并发操作,提升性能。支持整型、浮点、指针及满足平凡复制的自定义类型。核心操作包括load/store、fetch_add等读-改-写操作,以及compare_exchange_weak/strong实现无锁同步。内存序(memory order)控制操作的可见性和顺序:relaxed仅保证原子性;acquire/release配对使用,建立线程间happens-before关系;seq_cst为默认最强顺序一致性。实际应用需注意ABA问题(可用版本号规避)、伪共享(通过缓存行对齐)、避免混合原子与非原子访问、谨慎选择内存序以防可见性错误,并用is_lock_free判断是否真正无锁。

C++中利用std::atomic库进行原子操作,核心在于确保多线程环境下对共享变量的读写是不可中断的、原子的,从而避免数据竞争和未定义行为。它提供了比互斥锁更细粒度的同步机制,尤其适用于单个变量的并发操作,能够有效提升性能。
在C++多线程编程中,当我们处理共享数据时,std::atomic 提供了一种强大且高效的方式来保证操作的原子性。它通过利用底层硬件指令(如CAS, Compare-And-Swap)或在必要时使用轻量级锁,确保对变量的读、写或读-改-写操作作为一个整体完成,不会被其他线程中断。
要使用std::atomic,你需要包含<atomic>头文件。
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
// 声明一个原子计数器
std::atomic<int> global_counter(0);
void increment_counter() {
for (int i = 0; i < 100000; ++i) {
// 使用fetch_add进行原子加操作
// 这等价于 old_val = global_counter; global_counter = old_val + 1; 并保证整个过程原子性
global_counter.fetch_add(1);
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment_counter);
}
for (auto& t : threads) {
t.join();
}
std::cout << "最终计数器值: " << global_counter.load() << std::endl; // 使用load()原子读取
// 预期输出是 10 * 100000 = 1000000
// 也可以直接赋值和读取,它们也是原子操作
std::atomic<bool> flag(false);
flag.store(true); // 原子写入
if (flag.load()) { // 原子读取
std::cout << "Flag is true." << std::endl;
}
// 比较并交换 (CAS) 是原子操作的核心
std::atomic<int> value(10);
int expected = 10;
int desired = 20;
// 如果value当前是expected,就把它设置为desired,并返回true
// 否则,不改变value,并把value的当前值赋给expected,返回false
if (value.compare_exchange_strong(expected, desired)) {
std::cout << "CAS successful, value is now: " << value.load() << std::endl; // 20
} else {
std::cout << "CAS failed, value is still: " << value.load() << ", expected was: " << expected << std::endl;
}
expected = 20; // 再次尝试,这次expected是正确的
desired = 30;
if (value.compare_exchange_strong(expected, desired)) {
std::cout << "Another CAS successful, value is now: " << value.load() << std::endl; // 30
} else {
std::cout << "Another CAS failed." << std::endl;
}
return 0;
}我记得刚开始接触多线程编程时,总觉得一个std::mutex就能解决所有并发问题。毕竟,它能确保任何时刻只有一个线程进入临界区,听起来万无一失。然而,随着项目复杂度的增加,我逐渐意识到,互斥锁虽然强大,但并非总是最优解,甚至在某些场景下会成为性能瓶颈。
立即学习“C++免费学习笔记(深入)”;
原子操作和互斥锁解决的都是数据竞争问题,但它们的方式和适用场景大相径庭。
互斥锁(std::mutex)的工作原理是,它会锁定一个代码块,确保在任何给定时间只有一个线程可以执行该代码块。这很好,当你需要保护一个复杂的临界区,里面可能包含多个变量的修改、复杂的逻辑判断,或者涉及到I/O操作时,互斥锁是首选。它提供了一种粗粒度的同步,能够有效地管理复杂的共享状态。
原子操作(std::atomic)则不同,它专注于单个变量的操作。例如,对一个整数进行增量操作(i++),看似简单,但在多线程环境下,它实际上是“读取i的值”、“将i的值加1”、“将新值写回i”这三个步骤。如果这三个步骤不是原子的,另一个线程可能在中间读取到一个旧值,或者在写入前覆盖了你的中间结果,导致数据丢失或错误。std::atomic就是为了确保这“读取-修改-写入”的整个过程是不可分割的。
那为什么说互斥锁不够呢?
int,你也需要锁住整个临界区。这可能导致不必要的阻塞,因为其他线程可能在等待一个与它们无关的变量的锁释放。所以,当你的需求只是对一个简单的变量进行原子性的读、写或读-改-写操作时,std::atomic通常是更高效、更轻量级的选择。它通过直接利用CPU提供的原子指令(如LOCK XADD,CMPXCHG等)来实现,避免了操作系统层面的开销,性能上通常优于互斥锁。当然,如果你的操作涉及多个变量或复杂的逻辑,那么互斥锁依然是不可替代的。选择哪种,关键在于理解它们的底层机制和各自的适用场景。
std::atomic 支持哪些类型,以及其内存序(Memory Order)如何影响程序行为?std::atomic 并非支持所有类型,但它覆盖了绝大多数我们日常会用到的基本数据类型和指针类型。具体来说,它可以包装:
bool, char, short, int, long, long long 及其无符号版本。float, double, long double(虽然标准支持,但实际中原子操作在浮点数上可能需要软件模拟,性能不一定高)。T*,任何对象的指针。std::atomic包装:对于自定义类型,你可以通过std::atomic<MyStruct> my_atomic_struct;来使用。不过,std::atomic保证的是对MyStruct实例的整体读写是原子的,而不是其内部成员的原子性。如果MyStruct内部有多个成员需要独立原子访问,那可能需要更复杂的同步机制。
此外,std::atomic_flag 是一个非常特殊的原子类型,它只支持两种操作:test_and_set() 和 clear(),通常用于实现自旋锁,是所有原子类型中最简单、开销最小的。
内存序(Memory Order)如何影响程序行为?
这部分内容说实话,刚接触的时候真的有点让人头疼,因为它直接触及了编译器优化和CPU乱序执行的底层原理。简单来说,内存序就是用来告诉编译器和CPU,在多线程环境下,你的内存操作(读、写)应该以什么样的顺序被其他线程看到。它决定了不同线程之间数据可见性的保证强度。
C++11引入了六种内存序:
std::memory_order_relaxed (松散序):
relaxed操作,只要不改变当前线程的执行结果。std::memory_order_release (释放序):
std::memory_order_acquire配对使用。release语义。它保证所有在release操作之前的内存写入操作,都会在release操作完成之后对其他线程可见。release操作像一个“栅栏”,它之前的内存操作不能被重排到它之后。std::memory_order_acquire (获取序):
std::memory_order_release配对使用。acquire语义。它保证所有在acquire操作之后的内存读取操作,都会在acquire操作完成之后执行。同时,它能看到所有在配对的release操作之前的内存写入。acquire操作像另一个“栅栏”,它之后的内存操作不能被重排到它之前。acquire和release的配合使用,可以在两个线程间建立“happens-before”关系。一个线程的release操作happens-before另一个线程的acquire操作,那么release之前的所有内存写入,都会在acquire之后对获取线程可见。这是实现无锁数据结构的关键。
std::atomic<bool> ready_flag(false);
int data = 0;
void writer_thread() {
data = 42; // (1) 写入数据
ready_flag.store(true, std::memory_order_release); // (2) 释放操作
}
void reader_thread() {
while (!ready_flag.load(std::memory_order_acquire)) { // (3) 获取操作
std::this_thread::yield();
}
std::cout << "Data: " << data << std::endl; // (4) 读取数据
}
// 在这个例子中,由于release/acquire语义,(1) happens-before (2),(2) happens-before (3),(3) happens-before (4)。
// 所以,当reader线程看到ready_flag为true时,它保证能看到data = 42。std::memory_order_acq_rel (获取-释放序):
fetch_add、compare_exchange)。acquire和release的语义:它能看到所有在它之前的release操作的写入,并且它之前的写入操作都会在它完成之后对其他线程可见。std::memory_order_seq_cst (顺序一致性):
acquire和release的所有保证,还额外保证了所有seq_cst操作在所有线程中都以相同的全序执行。在实际开发中,如果对内存序没有深入理解,最安全的做法是使用默认的std::memory_order_seq_cst。只有在确认性能是瓶颈且对并发模型有充分理解时,才考虑使用更弱的内存序。过度优化内存序,往往会引入难以调试的并发bug。
在真实项目中,选择合适的原子操作并避免陷阱,是保证多线程程序正确性和性能的关键。这需要对std::atomic有比较深入的理解和实践经验。
选择合适的原子操作:
简单读写:load() 和 store()
atomic_var.load()和atomic_var.store(value)。std::memory_order_relaxed可能足够。但如果这个标志位的变化需要保证其他数据可见性,那么acquire/release或seq_cst是必要的。读-改-写(RMW)操作:fetch_add(), fetch_sub(), exchange() 等
fetch_add(1)会原子地将变量加1,并返回加1前的值。load()、modify、store()再加锁要高效得多,因为它们在硬件层面就能保证原子性。exchange(new_value):原子地将变量设置为new_value,并返回旧值。比较并交换(CAS):compare_exchange_weak() 和 compare_exchange_strong()
compare_exchange_strong(expected, desired):如果当前原子变量的值等于expected,则将其原子地设置为desired,并返回true;否则,不改变原子变量的值,并将当前值写入expected,返回false。compare_exchange_weak():与strong类似,但它可能在值相等时“虚假失败”(spurious failure),即返回false但实际上值是相等的。这通常发生在某些硬件架构上,为了性能考虑,它不会重试。它通常用在循环中,例如do { ... } while (!atomic_var.compare_exchange_weak(...));。weak可能性能更好,因为它避免了不必要的重试开销。如果CAS操作只执行一次,或者你不能容忍虚假失败,strong是更安全的选择。避免常见陷阱:
ABA问题:
std::atomic<std::shared_ptr<T>>可以自动处理ABA问题,因为它内部通常会维护一个版本计数器。伪共享(False Sharing):
alignas(std::hardware_destructive_interference_size)(C++17)或手动填充(padding)来确保不同的原子变量位于不同的缓存行。内存序的误用:
std::memory_order_relaxed或std::memory_order_acquire/release,导致程序在某些CPU架构或编译器优化下出现数据可见性问题,产生难以复现的bug。例如,忘记在release操作前写入数据,或在acquire操作后读取数据。std::memory_order_seq_cst: 这是最安全的选项,除非你确定需要优化性能并且对内存模型有深入理解。acquire/release语义: 牢记release操作前的所有写入对配对的acquire操作后的所有读取可见。如果你的数据流需要这种顺序保证,就必须使用它们。relaxed: 只有当你明确知道操作的顺序对其他线程不重要时才使用。混合原子与非原子访问:
std::atomic进行操作,有时又直接进行非原子操作。这会导致数据竞争,因为非原子操作不会受到任何同步保证。std::atomic,就应该始终通过其原子接口进行访问。并非所有原子操作都是无锁的:
std::atomic不保证其所有操作都是“无锁”的(即不使用操作系统互斥锁)。对于某些复杂类型或某些平台,std::atomic可能在内部使用互斥锁来模拟原子性。atomic_var.is_lock_free()来检查一个特定的std::atomic实例是否是真正无锁的。如果返回false,那么它的性能可能不如预期,甚至可能比std::mutex更差。总的来说,原子操作是C++并发编程的强大工具,但它并非银弹。理解其底层原理、内存序以及潜在的陷阱,并在实际项目中谨慎选择和使用,才能真正发挥其优势。
以上就是c++++如何使用原子操作atomic_c++多线程原子操作库应用的详细内容,更多请关注php中文网其它相关文章!
c++怎么学习?c++怎么入门?c++在哪学?c++怎么学才快?不用担心,这里为大家提供了c++速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号