C++ 原子操作
什么是原子操作?
在多线程编程中,当多个线程同时访问共享数据时,如果不加以控制,往往会导致数据竞争(Data Race)和不可预期的结果。原子操作就是一种能够在多线程环境中安全地进行读写操作的机制,它保证了操作的不可分割性,也就是说,一个原子操作要么完全执行完毕,要么完全不执行,中间状态对其他线程是不可见的。
原子(Atomic)一词来源于希腊语"atomos",意为"不可分割的",这也正是原子操作的本质特点。
C++ 11中的原子支持
C++11标准引入了对原子操作的直接支持,主要通过<atomic>
头文件提供。这个库定义了多个原子类型以及对它们的操作函数,使我们可以在不使用互斥锁的情况下实现线程安全的数据访问。
核心原子类型
C++标准库提供了几种常用的原子类型:
std::atomic<T>
- 可以将任何类型T变为原子类型std::atomic_flag
- 一个简单的布尔原子类型std::atomic_bool
,std::atomic_int
,std::atomic_uint
, 等等 - 常用基本类型的原子版本
基本原子操作示例
让我们从一个简单的例子开始,展示原子操作的基本用法:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 原子计数器
void increment_counter() {
for (int i = 0; i < 10000; ++i) {
counter++; // 原子递增操作
}
}
int main() {
std::vector<std::thread> threads;
// 创建5个线程,每个线程递增计数器10000次
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(increment_counter));
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << "最终计数值: " << counter << std::endl;
return 0;
}
输出结果:
最终计数值: 50000
在这个例子中,我们创建了一个原子整型变量counter
,然后启动5个线程,每个线程递增这个计数器10000次。由于counter
是原子类型,所以所有的递增操作都是安全的,最终结果总是准确的50000。
如果我们使用普通的int
而不是std::atomic<int>
,就会出现数据竞争,最终结果通常会小于50000,因为不同线程的递增操作可能会相互覆盖。
原子操作与内存序
C++的原子操作不仅仅是简单的读写,它还涉及到内存序(Memory Ordering)的概念,这决定了原子操作在多线程环境中如何与其他内存操作交互。
C++定义了六种内存序:
memory_order_relaxed
: 最宽松的内存序,只保证当前操作的原子性,不提供任何同步或顺序保证memory_order_consume
: 提供数据依赖关系的顺序保证(较少使用)memory_order_acquire
: 读取操作使用,保证之后的读写不会被重排到此操作之前memory_order_release
: 写入操作使用,保证之前的读写不会被重排到此操作之后memory_order_acq_rel
: 读写操作使用,同时具有acquire和release语义memory_order_seq_cst
: 最严格的内存序,提供全局一致的顺序(默认选项)
使用不同内存序的示例
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<bool> ready(false);
std::atomic<int> data(0);
void producer() {
// 准备数据
data.store(42, std::memory_order_relaxed);
// 通知消费者数据已准备好(使用release保证之前的写入不会被重排到这之后)
ready.store(true, std::memory_order_release);
}
void consumer() {
// 等待数据准备好(使用acquire保证之后的读取不会被重排到这之前)
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield(); // 让出CPU时间
}
// 此时可以安全地读取数据
std::cout << "消费者读取的数据: " << data.load(std::memory_order_relaxed) << std::endl;
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
输出结果:
消费者读取的数据: 42
这个例子演示了使用memory_order_release
和memory_order_acquire
建立线程间的同步关系。当生产者线程设置ready
为true时,消费者线程保证可以看到生产者在此之前对data
的修改。
原子操作的性能考量
原子操作虽然避免了使用互斥锁的开销,但它们仍然可能导致性能损失,特别是在多处理器系统上。这是因为原子操作通常需要处理器间的同步,可能导致缓存失效和内存屏障的开销。
当需要保护简单的操作(如递增计数器)时,原子操作通常比互斥锁更高效。但对于复杂的数据结构或多个相关变量,互斥锁可能是更好的选择。
实际应用案例:线程安全的单例模式
原子操作可以用于实现高效的双重检查锁定(Double-Checked Locking)单例模式:
#include <atomic>
#include <mutex>
#include <memory>
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
// 私有构造函数
Singleton() {
// 初始化代码
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
// 首次检查,不上锁
Singleton* expected = nullptr;
if (instance.load(std::memory_order_acquire) == nullptr) {
// 如果实例可能不存在,上锁并再次检查
std::lock_guard<std::mutex> lock(mtx);
expected = nullptr;
// 尝试原子地设置instance,仅当instance当前为nullptr时
if (instance.compare_exchange_strong(expected, new Singleton(),
std::memory_order_acq_rel)) {
// 当前线程负责创建实例
}
}
return instance.load(std::memory_order_acquire);
}
};
// 静态成员初始化
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
这个实现使用原子操作和互斥锁相结合,确保单例对象只被创建一次,同时最小化锁竞争的可能性。
原子标志 (atomic_flag)
std::atomic_flag
是C++中最简单的原子类型,它表示一个布尔标志,只能设置(set)和清除(clear)。它是唯一一个保证在所有平台上都是无锁实现的原子类型。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic_flag lock = ATOMIC_FLAG_INIT; // 必须初始化为清除状态
void critical_section(int id) {
// 自旋直到获得锁
while (lock.test_and_set(std::memory_order_acquire)) {
// 自旋等待
}
// 临界区
std::cout << "线程 " << id << " 进入临界区\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "线程 " << id << " 离开临界区\n";
// 释放锁
lock.clear(std::memory_order_release);
}
int main() {
std::vector<std::thread> threads;
// 创建5个线程,每个线程都尝试进入临界区
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(critical_section, i));
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
输出结果(顺序可能会有所不同):
线程 0 进入临界区
线程 0 离开临界区
线程 3 进入临界区
线程 3 离开临界区
线程 1 进入临界区
线程 1 离开临界区
线程 4 进入临界区
线程 4 离开临界区
线程 2 进入临界区
线程 2 离开临界区
在这个例子中,我们使用atomic_flag
实现了一个简单的自旋锁,确保同一时刻只有一个线程可以进入临界区。
Compare-And-Swap (CAS) 操作
原子类型提供了compare_exchange_weak
和compare_exchange_strong
方法,这是实现无锁数据结构的基础操作。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> value(0);
void update_if_equal(int expected, int new_value) {
// 只有当value等于expected时,才将value设置为new_value
if (value.compare_exchange_strong(expected, new_value)) {
std::cout << "成功更新了值到 " << new_value << std::endl;
} else {
std::cout << "更新失败,当前值为 " << expected << std::endl;
}
}
int main() {
// 初始值为0
std::cout << "初始值: " << value << std::endl;
// 尝试更新值
update_if_equal(0, 10); // 应该成功,因为当前值确实是0
update_if_equal(0, 20); // 应该失败,因为当前值已经是10了
return 0;
}
输出结果:
初始值: 0
成功更新了值到 10
更新失败,当前值为 10
compare_exchange_strong
函数比较原子变量当前值和提供的期望值,如果相等,它就替换为新值并返回true;如果不相等,它就更新期望值参数为当前值并返回false。
总结
C++原子操作是现代并发编程的重要工具,它们提供了一种在多线程环境中安全访问共享数据的方式,无需传统的互斥锁。关键点包括:
- 原子操作保证了操作的不可分割性
- C++11引入的
<atomic>
头文件提供了全面的原子类型支持 - 内存序决定了原子操作如何与其他内存操作交互
- 原子操作通常比互斥锁更轻量,但也有其性能考量
- 原子操作是实现无锁数据结构的基础
虽然原子操作功能强大,但也需要谨慎使用,特别是在涉及内存序选择时。一般来说,如果不确定使用哪种内存序,最好使用默认的memory_order_seq_cst
,虽然它可能有一定的性能开销,但能提供最强的保证。
练习与深入学习
- 练习1: 修改上面的计数器示例,使用
fetch_add
而不是operator++
,并比较两者的性能差异。 - 练习2: 实现一个原子的布尔变量,用于线程间的信号传递。
- 练习3: 尝试实现一个简单的自旋锁,并与
std::mutex
比较性能差异。
延伸阅读
- C++标准库参考: cppreference - std::atomic
- 内存模型详解: C++ Memory Model
- 《C++ Concurrency in Action》by Anthony Williams,这本书详细介绍了C++的并发特性,包括原子操作
通过掌握原子操作,你将能够编写更高效、更安全的并发代码,并为学习更复杂的无锁数据结构打下基础。