1 std::atomic 的加载与存储操作
加载和存储操作是 std::atomic<int> 最基础的两种操作。
加载操作
加载操作是通过 load 成员函数实现的,它用于从原子变量中读取一个值。这个函数可以确保在读取过程中,不会被其他线程打断。
std::atomic<int> atomic_var(10); // 初始化一个原子变量,初始值为 10
int value = atomic_var.load(); // 原子加载操作,将 atomic_var 的值加载到 value 中
存储操作
存储操作是通过 store 成员函数实现的,它用于向原子变量中写入一个值。这个函数可以确保在写入过程中,不会被其他线程打断。
std::atomic<int> atomic_var(10); // 初始化一个原子变量,初始值为 10
atomic_var.store(20); // 原子存储操作,将 20 存储到 atomic_var 中
示例
以下是一个综合示例,展示如何使用这两个函数安全地在多个线程之间共享和更新一个原子整数。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> shared_data(0); // 初始化一个原子整数为0
void increment(int n) {
for (int i = 0; i < n; ++i) {
shared_data.store(shared_data.load() + 1); // 原子加载,增加,然后存储
}
}
void decrement(int n) {
for (int i = 0; i < n; ++i) {
int value = shared_data.load(); // 原子加载当前值
while (!shared_data.compare_exchange_weak(value, value - 1)) {
// compare_exchange_weak 是一个原子比较和交换操作
// 如果当前值与预期的 value 相等,则将其替换为 value - 1
// 如果不相等,说明其他线程已经修改了值,重新加载并尝试
}
}
}
int main()
{
const int num_threads = 4;
const int increment_amount = 100;
const int decrement_amount = 50;
std::vector<std::thread> threads;
// 创建两个增加线程和两个减少线程
for (int i = 0; i < 2; ++i) {
threads.emplace_back(increment, increment_amount);
}
for (int i = 0; i < 2; ++i) {
threads.emplace_back(decrement, decrement_amount);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
int final_value = shared_data.load(); // 原子加载最终值
std::cout << "Final value of shared_data: " << final_value << std::endl;
return 0;
}
上面代码的输出为:
Final value of shared_data: 100
在这个示例中:
- shared_data 是一个原子整数,多个线程将对其进行增加和减少操作。
- increment 函数通过 load 加载当前值,增加它,然后使用 store 存储回原子变量中。
- decrement 函数使用了 compare_exchange_weak 函数,它执行一个比较和交换操作。这是一个更复杂的原子操作,它尝试将 shared_data 的当前值减少 1,但仅当该值仍然等于我们加载的值时。这有助于防止在减少过程中发生的数据竞争。
- 示例中创建了四个线程:两个用于增加 shared_data,两个用于减少它。
- 所有线程完成后,使用 load 函数获取 shared_data 的最终值,并打印出来。
注意,compare_exchange_weak 是一种更强大的原子操作,它通常用于实现更复杂的并发控制逻辑,如锁和无锁数据结构。在简单的增加和减少操作中,直接使用 load 和 store 也是可以的,但 compare_exchange_weak 可以提供更强大的保证,特别是在高并发环境下。
2 std::atomic 的交换操作
std::atomic<T>::exchange 函数是用于原子地交换 std::atomic<T> 对象的当前值与提供的值,并返回对象的原始值。原子操作在多线程编程中非常重要,因为它们保证了对共享数据的访问不会引发数据竞争,即多个线程同时读写数据导致的结果不可预测。
std::atomic<T>::exchange 的基本语法如下:
T exchange( T desired );
其中 desired 是需要与 std::atomic<T> 对象交换的值。这个函数会原子地执行以下操作:
- 保存 std::atomic<T> 对象的当前值。
- 将 std::atomic<T> 对象的值设置为 desired。
- 返回之前保存的 std::atomic<T> 对象的值。
这个过程是原子的,即它在多线程环境中是线程安全的,不会有其他线程能够在这个操作执行期间改变 std::atomic<T> 对象的状态。
下面是一个使用 std::atomic<T>::exchange 的简单示例:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 初始化原子计数器为0
void incrementAndExchange(int incrementBy, int exchangeWith) {
int oldValue = counter.exchange(exchangeWith); // 交换并获取旧值
oldValue += incrementBy; // 在这里可以对旧值进行操作,但这不会影响到原子对象
std::cout << "Thread " << std::this_thread::get_id() << " exchanged " << oldValue
<< " with " << exchangeWith << std::endl;
}
int main()
{
const int numThreads = 4;
std::vector<std::thread> threads;
// 创建四个线程,每个线程都会尝试交换计数器的值
for (int i = 0; i < numThreads; ++i) {
int increment = (i + 1) * 10; // 每个线程增加的值不同
int newValue = i * 100; // 每个线程想要交换的新值也不同
threads.emplace_back(incrementAndExchange, increment, newValue);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
int finalValue = counter.load(); // 原子加载最终值
std::cout << "Final value of counter: " << finalValue << std::endl;
return 0;
}
上面代码的输出为:
Thread 10308 exchanged 10 with 0
Thread 11188 exchanged 130 with 200
Thread 9836 exchanged 240 with 300
Thread 7020 exchanged 20 with 100
Final value of counter: 300
在这个示例中:
- 创建了一个 std::atomic<int> 类型的变量 counter,初始化为 0。
- 定义了一个 incrementAndExchange 函数,它接受两个参数:一个是要加到旧值上的增量,另一个是要与 counter 交换的新值。
- 在主函数中,创建了四个线程,每个线程都会调用 incrementAndExchange 函数,并传入不同的增量和新值。
- 每个线程都会原子地交换 counter 的值,并打印出交换前后的值。
- 最后,主线程通过 load 函数获取 counter 的最终值,并打印出来。
注意:由于多个线程同时尝试交换 counter 的值,因此最终值将取决于操作系统调度线程的顺序以及 exchange 操作的时机。因此,每次运行程序时,最终值都可能不同。
3 std::atomic 的比较并交换操作
std::atomic<T>::compare_exchange_strong 和 std::atomic<T>::compare_exchange_weak 都可以都可以实现原子地比较和交换(CAS)一个 std::atomic<T> 对象的值。尽管这两个函数在功能上有相似之处,但它们在语义和返回值上存在一些关键差异。
std::atomic::compare_exchange_strong
compare_exchange_strong 函数执行一个“比较并交换”操作,如果当前值与预期值(期望的旧值)相等,则原子地将该值更新为新值。它提供了一种强一致性的内存模型保证,确保不会出现虚假失败的情况。
函数的原型如下:
bool compare_exchange_strong( T& expected, T desired );
expected 是一个引用,用于存储当前 std::atomic<T> 对象的值。在调用此函数之前,你应该将 expected 设置为期望的当前值。
desired 是想要设置的新值。
如果 std::atomi\c 对象的当前值与 expected 相等,则对象的值会被设置为 desired,并且 expected 会被更新为对象的新值(即 desired)。函数返回 true 表示交换成功。如果当前值与 expected 不相等,则对象的值不会被改变,expected 会被更新为对象的当前值,函数返回 false。
std::atomic::compare_exchange_weak
compare_exchange_weak 函数与 compare_exchange_strong 功能相似,也是执行一个“比较并交换”操作。但是,compare_exchange_weak 提供了一种弱一致性的内存模型保证,它可能在某些情况下返回失败,即使当前值与预期值相等(即所谓的“虚假失败”)。这种“弱”版本在某些场景下可能性能更好,但使用它需要更小心,因为需要准备重新尝试操作直到成功。
函数的原型如下:
bool compare_exchange_weak( T& expected, T desired );
其参数和返回值与 compare_exchange_strong 相同。
综合示例
下面是一个使用 std::atomic<bool> 和 compare_exchange_weak 来实现自旋锁的示例,展示了如何在多线程环境中安全地增加一个整数的值:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
// 自旋锁的实现,使用std::atomic<bool>和compare_exchange_weak
class Spinlock {
public:
void lock() {
bool expected = false;
while (!locked_.compare_exchange_weak(expected, true)) {
// 如果锁被占用,则继续等待
expected = false; // 重置expected值,准备下一次比较交换
std::this_thread::yield(); // 可以让出CPU时间片
}
}
void unlock() {
locked_.store(false, std::memory_order_release);
}
private:
std::atomic<bool> locked_{ false };
};
// 临界区,需要保护的共享资源
int shared_resource = 0;
Spinlock spinlock;
void increment_shared_resource(int n) {
spinlock.lock();
for (int i = 0; i < n; ++i) {
++shared_resource;
}
spinlock.unlock();
}
int main()
{
const int numThreads = 4;
const int incrementAmount = 100000;
std::vector<std::thread> threads;
// 创建多个线程来增加共享资源
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(increment_shared_resource, incrementAmount);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出共享资源的最终值
std::cout << "Final value of shared_resource: " << shared_resource << std::endl;
return 0;
}
上面代码的输出为:
Final value of shared_resource: 400000
在这个示例中:
- Spinlock 类现在包含一个 std::atomic 类型的 locked_ 成员变量,用于表示锁的状态。
- lock 方法使用 compare_exchange_weak 尝试将 locked_ 的值从 false 更改为 true。如果当前值不是 false(即锁已经被其他线程占用),则 compare_exchange_weak 返回 false,并且 expected 被设置为当前值(在这个例子中,始终是 true)。然后,线程会忙等待(自旋),再次尝试获取锁。
- unlock 方法简单地将 locked_ 的值设置回 false,释放锁。
这个示例展示了如何使用 std::atomic::compare_exchange_weak 来实现一个简单的自旋锁。与 std::atomic_flag 相比,std::atomic 提供了更多的灵活性和控制,但通常 std::atomic_flag 在实现自旋锁时会有更好的性能,因为它专为测试-和-设置操作而设计,且不涉及内存排序的额外开销。在大多数情况下,std::atomic_flag 是自旋锁的首选。然而,了解如何使用 std::atomic::compare_exchange_weak 或 std::atomic::compare_exchange_strong 也是很有价值的,因为它们可以用于实现更复杂的同步原语或算法。
4 std::atomic 的原子算术操作
std::atomic<T> 提供了一系列成员函数,用于执行原子算术操作。这些操作可以应用于任何支持算术运算的数据类型。以下是针对 std::atomic<T> 的一些主要原子算术操作的详细解释:
- fetch_add(args): 将给定参数 args 加到当前值上,并返回操作前的值。
- fetch_sub(args): 从当前值中减去给定参数 args,并返回操作前的值。
- add_fetch(args): 将给定参数 args 加到当前值上,并返回操作后的值。
- sub_fetch(args): 从当前值中减去给定参数 args,并返回操作后的值。
下面是一个综合示例,演示了如何使用 std::atomic<int> 的原子算术操作:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
// 声明一个原子整数
std::atomic<int> atomicInt(0);
// 一个函数,用于在多个线程中增加 atomicInt 的值
void incrementAtomicInt(int n) {
for (int i = 0; i < n; ++i) {
// 使用 fetch_add 进行原子加法操作
int oldValue = atomicInt.fetch_add(1);
}
}
int main()
{
const int numThreads = 4;
const int incrementAmount = 10;
std::vector<std::thread> threads;
// 创建多个线程来执行 incrementAtomicInt 函数
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementAtomicInt, incrementAmount);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出 atomicInt 的最终值
std::cout << "Final value of atomicInt: " << atomicInt << std::endl;
return 0;
}
上面代码的输出为:
Final value of atomicInt: 40
在这个示例中:
- 定义了一个全局的 std::atomic<int> 变量 atomicInt,并初始化为 0。
- incrementAtomicInt 函数被多个线程调用,每个线程使用 fetch_add 方法原子地增加 atomicInt 的值,并输出操作前后的值。
- 在 main 函数中,创建了多个线程,每个线程执行 incrementAtomicInt 函数。
- 所有线程执行完毕后,输出 atomicInt 的最终值。由于使用了原子操作,即使多个线程同时修改 atomicInt,最终的结果也是正确的,即 numThreads * incrementAmount。
5 std::atomic 的原子位操作
除了原子算术操作外,std::atomic<T> 还支持原子位操作。位操作是对数据的位级别进行的操作,包括位与(bitwise AND)、位或(bitwise OR)、位异或(bitwise XOR)、位取反(bitwise NOT)、位移等。在并发编程中,原子位操作特别有用,因为它们允许程序员以非常精细的方式控制并发访问的数据。
std::atomic<T> 提供了以下位操作相关的成员函数:
- fetch_and(T desired): 对当前值执行原子位与操作,并返回操作前的值。
- fetch_or(T desired): 对当前值执行原子位或操作,并返回操作前的值。
- fetch_xor(T desired): 对当前值执行原子位异或操作,并返回操作前的值。
- and_fetch(T desired): 对当前值执行原子位与操作,并返回操作后的值。
- or_fetch(T desired): 对当前值执行原子位或操作,并返回操作后的值。
- xor_fetch(T desired): 对当前值执行原子位异或操作,并返回操作后的值。
下面是一个综合示例,展示了如何使用 std::atomic<T> 的原子位操作:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <bitset>
// 声明一个原子整数
std::atomic<int> atomicInt(0b1010); // 二进制表示,初始值为 1010
// 一个函数,用于在多个线程中修改 atomicInt 的值,通过原子位操作
void modifyAtomicInt(int mask) {
// 使用 fetch_and 执行原子位与操作
int oldValue = atomicInt.fetch_and(mask);
// 使用 fetch_or 执行原子位或操作
oldValue = atomicInt.fetch_or(~mask); // ~mask 是 mask 的位取反
}
int main()
{
const int numThreads = 4;
const int mask = 0b0101; // 二进制掩码
std::vector<std::thread> threads;
// 创建多个线程来执行 modifyAtomicInt 函数
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(modifyAtomicInt, mask);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出 atomicInt 的最终值
std::cout << "Final value of atomicInt: " << std::bitset<32>(atomicInt) << std::endl;
return 0;
}
上面代码的输出为:
Final value of atomicInt: 11111111111111111111111111111010
在这个示例中:
- 定义了一个全局的 std::atomic<int> 变量 atomicInt,并初始化为二进制值 1010。
- modifyAtomicInt 函数被多个线程调用,每个线程使用 fetch_and 和 fetch_or 方法原子地修改 atomicInt 的值,并输出操作前后的值。这里使用了 std::bitset 来以二进制形式输出整数值。
- 在 main 函数中,创建了多个线程,每个线程执行 modifyAtomicInt 函数,并传入一个二进制掩码 mask。
- 所有线程执行完毕后,输出 atomicInt 的最终值