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,你需要包含头文件。
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
// 声明一个原子计数器
std::atomic<int> global_counter(0);
void increment_counter() {
for (int i = 0; i threads;
for (int i = 0; i flag(false);
flag.store(true); // 原子写入
if (flag.load()) { // 原子读取
std::cout 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 <h3>为什么我们需要原子操作,互斥锁不够吗?</h3>
<p>我记得刚开始接触多线程编程时,总觉得一个<code>std::mutex</code>就能解决所有并发问题。毕竟,它能确保任何时刻只有一个线程进入临界区,听起来万无一失。然而,随着项目复杂度的增加,我逐渐意识到,互斥锁虽然强大,但并非总是最优解,甚至在某些场景下会成为<a style="color:#f60; text-decoration:underline;" title="性能瓶颈" href="https://m.php.cn/zt/33411.html" target="_blank">性能瓶颈</a>。</p>
<p>原子操作和互斥锁解决的都是数据竞争问题,但它们的方式和适用场景大相径庭。</p>
<p><strong>互斥锁(<code>std::mutex</code>)的工作原理</strong>是,它会锁定一个代码块,确保在任何给定时间只有一个线程可以执行该代码块。这很好,当你需要保护一个复杂的临界区,里面可能包含多个变量的修改、复杂的逻辑判断,或者涉及到I/O操作时,互斥锁是首选。它提供了一种粗粒度的同步,能够有效地管理复杂的共享状态。</p>
<p><strong>原子操作(<code>std::atomic</code>)则不同</strong>,它专注于单个变量的操作。例如,对一个整数进行增量操作(<code>i++</code>),看似简单,但在多线程环境下,它实际上是“读取i的值”、“将i的值加1”、“将新值写回i”这三个步骤。如果这三个步骤不是原子的,另一个线程可能在中间读取到一个旧值,或者在写入前覆盖了你的中间结果,导致<a style="color:#f60; text-decoration:underline;" title="数据丢失" href="https://m.php.cn/zt/38926.html" target="_blank">数据丢失</a>或错误。<code>std::atomic</code>就是为了确保这“读取-修改-写入”的整个过程是不可分割的。</p>
<p><strong>那为什么说互斥锁不够呢?</strong></p>
<ol>
<li>
<strong>性能开销:</strong> 互斥锁的开销相对较大。每次加锁和解锁都可能涉及到<a style="color:#f60; text-decoration:underline;" title="操作系统" href="https://m.php.cn/zt/16016.html" target="_blank">操作系统</a>层面的上下文切换,这在频繁操作时会带来显著的性能损失。特别是在高并发、临界区很短(比如只是对一个计数器加1)的场景下,互斥锁的同步成本可能远超实际业务逻辑的执行成本。</li>
<li>
<strong>粒度问题:</strong> 互斥锁是粗粒度的。即使你只是想原子地更新一个<code>int</code>,你也需要锁住整个临界区。这可能导致不必要的阻塞,因为其他线程可能在等待一个与它们无关的变量的锁释放。</li>
<li>
<strong>死锁风险:</strong> 互斥锁如果使用不当,很容易引入死锁。例如,如果两个线程各自持有对方需要的锁,就会陷入僵局。原子操作则没有死锁的概念,因为它不涉及资源的“持有”和“等待”。</li>
</ol>
<p>所以,当你的需求只是对一个简单的变量进行原子性的读、写或读-改-写操作时,<code>std::atomic</code>通常是更高效、更轻量级的选择。它通过直接利用CPU提供的原子指令(如<code>LOCK XADD</code>,<code>CMPXCHG</code>等)来实现,避免了操作系统层面的开销,性能上通常优于互斥锁。当然,如果你的操作涉及多个变量或复杂的逻辑,那么互斥锁依然是不可替代的。选择哪种,关键在于理解它们的底层机制和各自的适用场景。</p>
<h3>
<code>std::atomic</code> 支持哪些类型,以及其内存序(Memory Order)如何影响程序行为?</h3>
<p><code>std::atomic</code> 并非支持所有类型,但它覆盖了绝大多数我们日常会用到的基本数据类型和指针类型。具体来说,它可以包装:</p>
<ul>
<li>
<strong>所有基本整数类型:</strong> <code>bool</code>, <code>char</code>, <code>short</code>, <code>int</code>, <code>long</code>, <code>long long</code> 及其无符号版本。</li>
<li>
<strong>浮点类型:</strong> <code>float</code>, <code>double</code>, <code>long double</code>(虽然标准支持,但实际中原子操作在浮点数上可能需要软件模拟,性能不一定高)。</li>
<li>
<strong>指针类型:</strong> <code>T*</code>,任何对象的指针。</li>
<li>
<strong>自定义类型:</strong> 如果自定义类型满足以下条件,也可以被<code>std::atomic</code>包装:<ul>
<li>没有用户定义的拷贝赋值运算符。</li>
<li>没有用户定义的移动赋值运算符。</li>
<li>没有用户定义的析构函数。</li>
<li>是可平凡复制的(Trivially Copyable)。</li>
<li>所有非静态数据成员都是可平凡复制的。</li>
<li>通常,这意味着你的自定义类型应该是一个简单的结构体,只包含基本类型或指针,并且不涉及复杂的资源管理。</li>
</ul>
</li>
</ul>
<p>对于自定义类型,你可以通过<code>std::atomic<mystruct> my_atomic_struct;</mystruct></code>来使用。不过,<code>std::atomic</code>保证的是对<code>MyStruct</code>实例的整体读写是原子的,而不是其内部成员的原子性。如果<code>MyStruct</code>内部有多个成员需要独立原子访问,那可能需要更复杂的同步机制。</p>
<p>此外,<code>std::atomic_flag</code> 是一个非常特殊的原子类型,它只支持两种操作:<code>test_and_set()</code> 和 <code>clear()</code>,通常用于实现自旋锁,是所有原子类型中最简单、开销最小的。</p>
<p><strong>内存序(Memory Order)如何影响程序行为?</strong></p>
<p>这部分内容说实话,刚接触的时候真的有点让人头疼,因为它直接触及了编译器优化和CPU乱序执行的底层原理。简单来说,内存序就是用来告诉编译器和CPU,在多线程环境下,你的内存操作(读、写)应该以什么样的顺序被其他线程看到。它决定了不同线程之间数据可见性的保证强度。</p>
<p>C++11引入了六种内存序:</p>
<ol>
<li>
<p><strong><code>std::memory_order_relaxed</code> (松散序)</strong>:</p>
<ul>
<li>这是最弱的内存序。它只保证操作本身的原子性,不提供任何跨线程的内存同步或顺序保证。</li>
<li>编译器和CPU可以随意重排<code>relaxed</code>操作,只要不改变当前线程的执行结果。</li>
<li>
<strong>适用场景:</strong> 当你只需要一个原子计数器,而不在乎计数器更新的顺序,也不需要这个计数器与其他内存操作建立任何顺序关系时。例如,统计某个事件发生的次数。</li>
</ul>
</li>
<li>
<p><strong><code>std::memory_order_release</code> (释放序)</strong>:</p>
<ul>
<li>与<code>std::memory_order_acquire</code>配对使用。</li>
<li>
<strong>写操作</strong>使用<code>release</code>语义。它保证所有在<code>release</code>操作<strong>之前</strong>的内存写入操作,都会在<code>release</code>操作完成<strong>之后</strong>对其他线程可见。</li>
<li>可以理解为,<code>release</code>操作像一个“栅栏”,它之前的内存操作不能被重排到它之后。</li>
</ul>
</li>
<li>
<p><strong><code>std::memory_order_acquire</code> (获取序)</strong>:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="overflowclass">C++14</a>
<p class="overflowclass">C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
<ul>
<li>与<code>std::memory_order_release</code>配对使用。</li>
<li>
<strong>读操作</strong>使用<code>acquire</code>语义。它保证所有在<code>acquire</code>操作<strong>之后</strong>的内存读取操作,都会在<code>acquire</code>操作完成<strong>之后</strong>执行。同时,它能看到所有在<strong>配对的<code>release</code>操作之前</strong>的内存写入。</li>
<li>
<code>acquire</code>操作像另一个“栅栏”,它之后的内存操作不能被重排到它之前。</li>
</ul>
<p><strong><code>acquire</code>和<code>release</code>的配合使用</strong>,可以在两个线程间建立“happens-before”关系。一个线程的<code>release</code>操作<strong>happens-before</strong>另一个线程的<code>acquire</code>操作,那么<code>release</code>之前的所有内存写入,都会在<code>acquire</code>之后对获取线程可见。这是实现无锁数据结构的关键。</p>
<pre class="brush:cpp;toolbar:false;">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 </bool>
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()等- 当你需要在一个原子操作中读取变量的值,然后基于这个值进行修改,再写回变量时,RMW操作是你的朋友。
- 例如,
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(...));。 -
选择: 如果你在一个循环中反复尝试CAS,
weak可能性能更好,因为它避免了不必要的重试开销。如果CAS操作只执行一次,或者你不能容忍虚假失败,strong是更安全的选择。
避免常见陷阱:
-
ABA问题:
- 描述: 假设线程A读取变量X的值为A,然后被调度出去。线程B修改X为B,然后又改回A。线程A恢复执行,发现X的值仍然是A,认为没有被修改过,然后执行CAS操作成功。但实际上,X在中间被修改过。这在一些无锁数据结构中可能导致逻辑错误,例如,一个节点被弹出后又被重新插入到链表中,导致线程A操作了一个“陈旧”的指针。
-
规避:
- 使用版本号或标记:将原子变量包装成一个结构体,包含实际值和一个版本号(或计数器)。每次修改值时,同时递增版本号。CAS操作时,同时比较值和版本号。
- C++标准库中的
std::atomic<:shared_ptr>>可以自动处理ABA问题,因为它内部通常会维护一个版本计数器。
-
伪共享(False Sharing):
- 描述: 多个线程访问不同的原子变量,但这些原子变量恰好位于同一个CPU缓存行(cache line)中。当一个线程修改其原子变量时,整个缓存行会被标记为脏,并需要同步到其他CPU核心,导致其他核心的缓存失效。即使这些线程访问的是完全不同的数据,但由于它们共享同一个缓存行,也会引发不必要的缓存同步开销,严重影响性能。
-
规避:
-
缓存行对齐: 使用
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++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










