C++ 中 std::atomic 与 Volatile 的区别

2025年03月22日 | 阅读 10 分钟

在 C++ 中,std::atomic 通过提供原子性来确保变量操作的线程安全。相反,volatile 阻止编译器对变量访问进行过度优化。它不能保证线程安全。std::atomic 旨在满足并发需求,而 volatile 主要用于与硬件交互。在本文中,我们将讨论 C++ 中 std::atomicVolatile 之间的区别。在讨论它们的区别之前,我们必须了解 C++ 中的 std::atomic 和 Volatile,以及它们的特性、优点和缺点。

什么是 std::atomic?

C++ 中的 std::atomic 类模板支持对数据类型的原子操作,确保它们作为一个单一的、不可分割的单元执行。这在使用并发编程时至关重要,因为它允许多个线程访问共享资源。原子操作确保任何线程都无法观察到操作的部分状态。

主要特点

std::atomic 函数的几个关键特性如下:

  1. 原子性: 当多个线程并发访问共享数据时,std::atomic 对象会无中断地运行,从而避免了竞态条件。原子性意味着任何操作的中间状态都不会被其他线程看到。
  2. 内存排序: std::atomic 提供内存排序控制,该控制决定了线程执行操作的可见性和顺序。当内存一致性和排序限制至关重要时,这一点至关重要。
  3. 无锁编程: std::atomic 使得编程无需依赖传统的锁(如 std::mutex)来维护数据完整性。这可以防止死锁和优先级反转,并减少开销。
  4. CAS (Compare-And-Swap): CAS(比较并交换)是一种基本的原子操作,仅当值与预期值匹配时才更新该值。当多个线程同时尝试更新共享资源时,这非常有用。atomic 提供了 compare_exchange_strong() 和 compare_exchange_weak() 等方法来处理这种情况。
  5. 线程安全的增/减: 即使多个线程尝试同时修改该值,std::atomic 函数也能确保更新正确执行。它允许对原子类型执行 ++(递增)或 --(递减)等操作,并能跨多个线程安全地执行。

std::atomic 的优点

std::atomic 函数的几个优点如下:

  1. 线程安全
    std::atomic 保证对变量的操作是原子的,这意味着它们作为一个单一的操作完成,不受其他线程的干扰。这消除了正在访问共享数据的线程之间的数据竞争
  2. 性能
    与传统的锁定技术(如互斥锁)相比,原子操作可能更有效,尤其是对于基本数据类型,因为它们没有锁定和解锁的开销。
  3. 无锁编程
    std::atomic 设计了大量的无锁操作。在争用较高的系统上,通过防止线程等待锁被释放,可以提高性能。
  4. 简单性
    std::atomic 函数可以简化需要同步的代码。它提供了一种简单的方法来执行增量或比较值等操作,而无需复杂的锁定逻辑。
  5. 控制内存顺序
    通过使用 std::atomic 提供的内存排序选项,如 memory_order_relaxed、memory_order_acquire、memory_order_release 等,开发人员可以控制原子变量上的操作如何与跨线程的内存操作交互。这种适应性可以提高性能。

std::atomic 的缺点

std::atomic 函数的几个缺点如下:

  1. 有限的用例
    std::atomic 最适合简单类型(如整数和指针)。对于复杂或非平凡可复制的类型,使用 std::atomic 可能更困难,甚至不可能。
  2. 高级操作的复杂性增加
    虽然基本操作很简单,但实现更复杂的数据结构(如链表或哈希映射)时,std::atomic 可能会变得复杂且容易出错。与使用互斥锁相比,它通常会导致更复杂的设计。
  3. 伪共享
    伪共享发生在内存中相邻的多个 std::atomic 变量因为它们位于同一个缓存行上而导致访问不同原子变量的线程之间发生阻塞。由于多个核心不断使彼此的缓存行失效,性能可能会受到影响。
  4. 代码维护复杂性
    尽管 std::atomic 使一些并发编程任务更容易,但正确地处理它仍然需要仔细注意。程序员必须控制内存一致性并确保操作按正确的顺序执行,如果处理不当可能会导致细微的错误。对于复杂的多线程逻辑,单独的原子操作可能不足以满足需求,可能还需要额外的同步机制。
  5. 缺乏细粒度控制
    C++ 内存模型(memory_order_relaxed、memory_order_acquire 等)提供了对单个原子操作内存排序的细粒度控制;std::atomic 不提供这种控制。对于希望使用独特同步技术的开发人员来说,这可能会限制他们的优化选择。

示例

让我们通过一个例子来说明 C++ 中的 std::atomic 函数。

输出

 
The final counter value is: 2000   

解释

使用 C++ 的 std::atomic 对共享变量进行安全的并发操作,提供的代码演示了如何创建一个基本的多线程程序。全局原子整数计数器首先设置为零。每个线程的增量函数使用具有宽松内存排序的 fetch_add 方法,执行一个循环,将计数器增加 1000 次。为了并发运行增量函数,在主函数中创建了两个线程(t1 和 t2)。一旦两个线程都完成了它们的执行(通过 join() 调用),就会打印计数器的最终值。通过使用 std::atomic,可以避免竞态条件,并确保最终计数器值的准确性,因为增量是原子地执行的。输出“The final counter value is: 2000”应该表明两个线程都正确地增加了计数器。

什么是 Volatile?

C++ 中的**“volatile”**类型限定符表示变量的值可能随时发生变化,而不管程序的控制流如何。它告诉编译器在生成代码时不要优化该变量,因此对变量的所有读写操作都将精确地按照代码中的指定执行。这对于内存值可能被外部进程或硬件更改的系统至关重要。

Volatile 的特性

volatile 函数的几个关键特性如下:

  1. 防止编译器优化
    volatile 变量的主要特性是它指示编译器避免优化对它们的读写操作。它表示每次在代码中引用该变量时,编译器都会生成指令,将值读入或写出内存,而不会缓存该值。
  2. 用于硬件访问或内存映射 I/O
    在嵌入式系统中,volatile 经常在处理硬件、寄存器和内存映射 I/O 时使用。因为值可能会在程序控制之外发生变化,所以它确保应用程序直接从内存中读取硬件值。
  3. 指示外部修改
    它用于变量的值可能被外部源(如硬件、另一个线程(尽管 volatile 本身不能保证线程安全)或中断服务例程)修改的情况。关于访问之间的值稳定性,编译器不应做任何假设。
  4. 多次读/写不被优化
    如果不存在 volatile,编译器可能会优化对同一内存位置的连续读写操作,假设在代码块执行期间该值保持不变。使用 volatile 时,每次读写都会严格按照源代码中的定义执行。
  5. 不保证线程安全
    尽管 volatile 确保每次访问都进行,但它不能保证线程安全和原子性。当多个线程在没有适当同步机制的情况下共享 volatile 变量时,可能会导致竞态条件和非原子操作。
  6. 防止缓存
    为了防止 CPU 将 volatile 变量的值缓存在寄存器中,每次访问 volatile 变量的值总是从内存中获取。当发生可能修改该值的外部事件(CPU 知晓)时,这一点变得很重要。

Volatile 的优点

volatile 函数的几个优点如下:

  1. 支持异步事件处理
    在实时系统中,异步事件可能会更改变量,例如计算机信号、硬件中断或其他发生在常规程序流之外的事件。通过将这些变量指定为 volatile,程序可以实时检测到外部进程所做的更改,从而确保每次读写都直接从内存中进行。
  2. 跨线程通信(谨慎使用)
    由于它不保证原子性或内存排序,volatile 不适用于多线程同步。尽管在某些特定情况下,当线程只需要使用信号进行简单通信时,它可能很有用。考虑使用 volatile 的布尔标志来指示线程何时应停止。然而,使用 volatile 时应谨慎,因为它本身不提供线程安全。
  3. 简化嵌入式系统调试
    使用 volatile 可以帮助阻止编译器优化掉重要的内存访问,这使得在调试嵌入式系统时更容易在运行时查看程序的实际状态。
  4. 编译器辅助的低级编程
    对于处理硬件中断和其他根据周围情况提供可变流程的系统,volatile 是必需的。在硬件控制应用程序中,通常需要频繁的内存访问。编译器通过以不同的方式处理 volatile 变量来考虑此要求。
  5. 有助于避免缓存问题
    出于性能原因(例如,特定的实时或硬件通信协议),某些变量可能代表不能被缓存的数据。将这些变量指定为 volatile 可以确保软件始终从内存中检索最新值,并绕过任何缓存机制,从而防止读取陈旧数据。

Volatile 的缺点

volatile 函数的几个缺点如下:

  1. 无内存排序保证
    volatile 不能保证内存排序和同步。当在多线程环境中对 volatile 变量所做的更改未按正确顺序反映给其他线程时,可能会导致不可预测的行为。
  2. 有限的用例
    volatile 主要用于低级编程任务,例如管理内存映射的输入/输出和访问硬件寄存器。由于它不适用于处理线程间通信或并发管理,因此在现代软件开发中的应用受到限制。
  3. 对并发具有误导性
    虽然使用 volatile 仅能阻止编译器优化掉对变量的访问,但开发人员可能会错误地认为它能保证线程安全。在需要锁(std::mutex)或其他适当的同步机制(如 std::atomic)的多线程程序中,这可能会导致错误。
  4. 无优化控制
    尽管 volatile 可以防止编译器优化对变量的访问,但它不会影响 CPU 和内存系统(包括缓存和指令重排序)的行为。当多个线程同时访问 volatile 变量时,仍然可能导致不一致的行为。

示例

让我们通过一个例子来说明 C++ 中的 Volatile。

输出

 
The main thread doing some work...
The worker thread started, waiting for the stop signal...
Sending stop signal to the worker thread...
Stop signal received. Worker thread terminating...
The worker thread was terminated. Program finished.   

解释

此 C++ 代码说明了如何使用 volatile 标志(Flag)来控制工作线程的终止。在循环中,工作线程持续检查该标志,模拟工作直到该标志被设置为 true。主线程运行两秒钟后将 Flag 设置为 true,指示工作线程停止。当标志被设置时,工作线程退出循环并终止。最后,主线程使用 join() 等待工作线程完成,从而完成程序。

C++ 中 std::atomic 和 Volatile 的主要区别

Difference between std::atomic and Volatile in C++

C++ 中 std::atomic 和 Volatile 之间有几个主要区别。一些主要区别如下:

特性std::atomicVolatile
目的它确保了多线程环境下的原子操作,从而防止了数据竞争。它用于修改可以更改其值的变量,并且该变量没有恒定值。
线程安全它使用原子操作来提供线程安全,确保线程之间的一致更新。它不提供线程安全;当多个线程同时运行时,仍然可能发生数据竞争。
用例它最适合多线程环境下的共享变量,其中原子读写操作至关重要。它用于硬件寄存器或内存映射 I/O,其中值可能不可预测地波动。
性能它对于线程安全是必需的,但由于原子性和内存排序的保证,通常比 volatile 慢。它只需要原子性和同步的开销,这使其比原子操作更快。
修改编译器在读写 std::atomic 时确保线程同步和原子性。由于编译器,volatile 变量仍然可以被多个线程修改而无需同步。
内存排序它支持对内存排序进行细粒度控制(带有内存排序参数)。它不提供同步方法或内存排序保证。
优化它防止了损害原子性的编译器优化。它仅阻止编译器优化。它不会影响 CPU 优化,例如缓存。

结论

总之,虽然 volatile 只确保编译器不会优化掉对变量的访问,但它不提供线程同步。另一方面,std::atomic 用于并发编程中的线程安全操作。


下一个主题C++ 中的翻牌游戏