本文转自:https://blog.csdn.net/qq_21438461/article/details/131338900
引言
在并发编程中,数据竞争(Data Race)是一个常见的问题。为了解决这个问题,C++11引入了一个新的库类型:std::atomic(原子类型)。这个类型提供了一种方式来保证对某些数据类型的操作是原子的,即这些操作在执行过程中不会被其他线程中断。
在英语口语交流中,我们通常会这样描述std::atomic: “The std::atomic type in C++ provides a way to perform atomic operations on certain data types, which means these operations cannot be interrupted by other threads during their execution.” (C++中的std::atomic类型提供了一种在某些数据类型上执行原子操作的方式,这意味着这些操作在执行过程中不能被其他线程中断。)
std::atomic类的函数原型
std::atomic类提供了一系列的成员函数和非成员函数,用于执行各种原子操作。
成员函数
以下是std::atomic的一些主要成员函数:
成员函数 | 描述 | 英文描述 |
---|---|---|
load | 读取存储的值 | Read the stored value |
store | 存储新的值 | Store a new value |
exchange | 存储新的值,并返回旧的值 | Store a new value and return the old value |
compare_exchange_weak | 比较并交换值(弱版本) | Compare and exchange values (weak version) |
compare_exchange_strong | 比较并交换值(强版本) | Compare and exchange values (strong version) |
operator= | 赋值操作符 | Assignment operator |
operator T | 类型转换操作符 | Type conversion operator |
operator++, operator– | 自增和自减操作符 | Increment and decrement operators |
operator+= , operator-= , operator&= , operator = , operator^= | 复合赋值操作符 |
非成员函数
std::atomic也有一些非成员函数,如std::atomic_is_lock_free,std::atomic_thread_fence,std::atomic_signal_fence等。这些函数主要用于查询原子类型的属性或者控制内存访问的顺序。
在英语口语交流中,我们通常会这样描述std::atomic的成员函数和非成员函数:“The std::atomic class provides a set of member functions and non-member functions for performing various atomic operations. The member functions include load, store, exchange, compare_exchange_weak, compare_exchange_strong, etc. The non-member functions include std::atomic_is_lock_free, std::atomic_thread_fence, std::atomic_signal_fence, etc.” (std::atomic类提供了一套成员函数和非成员函数,用于执行各种原子操作。成员函数包括load,store,exchange,compare_exchange_weak,compare_exchange_strong等。非成员函数包括std::atomic_is_lock_free,std::atomic_thread_fence,std::atomic_signal_fence等。)
std::atomic_bool
我觉得最常用的还是布尔型了,这边介绍一下注意事项
std::atomic_bool和std::atomic 的区别
在C++中,std::atomic_bool和std::atomic实际上是等价的。std::atomic_bool是std::atomic的类型别名,这意味着它们是完全相同的类型。在C++标准库中,为了方便使用,为一些常用的类型提供了类型别名,例如std::atomic_int是std::atomic的类型别名,std::atomic_long是std::atomic的类型别名,等等。
这两种方式在使用上没有区别,你可以根据自己的喜好选择使用哪一种。例如,你可以这样声明和使用一个原子布尔变量:
std::atomic_bool b1(false);
std::atomic<bool> b2(false);
b1.store(true);
b2.store(true);
bool v1 = b1.load();
bool v2 = b2.load();
在这个例子中,b1和b2是完全相同的类型,它们的行为也是完全相同的。
初始化操作
std::atomic的初始化需要在构造函数的初始化列表中进行,而不能在类的成员变量声明处进行。这是因为std::atomic没有默认的拷贝构造函数,所以不能在类的成员变量声明处进行初始化。
以下是如何在类中使用std::atomic的示例:
#include <atomic>
class MyClass {
public:
MyClass() : a(false) {} // 在构造函数的初始化列表中初始化a
void set(bool value) {
a.store(value); // 设置值
}
bool get() const {
return a.load(); // 获取值
}
private:
std::atomic<bool> a; // 声明一个std::atomic<bool>成员变量
};
在这个例子中,std::atomic成员变量a在MyClass的构造函数的初始化列表中被初始化为false。然后,你可以使用set和get成员函数来设置和获取a的值。这些操作都是线程安全的。
由于没有std::atomic没有拷贝构造函数,这意味着你不能通过复制一个已存在的std::atomic对象来创建一个新的std::atomic对象。
当你在类的成员变量声明处直接初始化一个std::atomic对象时,例如:
class MyClass {
private:
std::atomic<bool> a = false; // 这里会报错
};
这种语法实际上是在尝试使用拷贝构造函数来创建a。编译器会首先创建一个临时的std::atomic对象,然后尝试使用拷贝构造函数来创建a。但是,因为std::atomic没有拷贝构造函数,所以这会导致编译错误。
然而,当你在构造函数的初始化列表中初始化一个std::atomic对象时,例如:
class MyClass {
public:
MyClass() : a(false) {} // 这里不会报错
private:
std::atomic<bool> a;
};
这种语法是在直接调用std::atomic的构造函数来创建a,而不是尝试使用拷贝构造函数。因此,这不会导致编译错误。
赋值操作
对于std::atomic对象,你可以使用=操作符来进行赋值操作,这是线程安全的。例如:
std::atomic<bool> a(false); // 初始化为false
a = true; // 设置值为true
在这个例子中,=操作符用于设置std::atomic对象的值,这个操作是线程安全的。
然而,std::atomic类也提供了store和load成员函数,这些函数提供了一种更明确的方式来进行赋值和读取操作。例如:
std::atomic<bool> a(false); // 初始化为false
a.store(true); // 设置值为true
bool b = a.load(); // 获取值
在这个例子中,store函数用于设置std::atomic对象的值,load函数用于获取它的值。这两个操作都是线程安全的。
总的来说,对于std::atomic对象,你可以选择使用=操作符或者store和load函数来进行赋值和读取操作。这两种方式都是线程安全的,但store和load函数提供了一种更明确的方式来表达你的意图。
另外注意,a.load() = true
这样的写法是错误的,这是因为load()函数返回的是std::atomic对象的值的副本,而不是引用,所以你不能直接对它赋值。
如果你想改变std::atomic对象的值,你应该使用store函数或者=操作符,如下所示:
std::atomic<bool> a(false); // 初始化为false
a.store(true); // 使用store函数设置值为true
a = true; // 使用=操作符设置值为true
在这两个例子中,std::atomic对象的值被设置为true。这两种方式都是线程安全的。
读取操作
a.load()操作是线程安全的。这意味着你可以在多线程环境中安全地读取std::atomic对象的值。例如:
std::atomic<bool> a(false); // 初始化为false
bool b = a.load(); // 获取值
在这个例子中,load函数用于获取std::atomic对象的值,这个操作是线程安全的,即使在其他线程可能正在使用store函数改变a的值的情况下,load函数也能正确地获取a的值。
然而,你也可以直接使用=操作符来获取std::atomic对象的值,这也是线程安全的。例如:
std::atomic<bool> a(false); // 初始化为false
bool b = a; // 获取值
为什么std::atomic没有拷贝构造函数
在C++中,如果你没有为类提供拷贝构造函数,编译器会自动为你生成一个。然而,对于std::atomic类,拷贝构造函数被显式地删除了。
这是因为std::atomic类被设计为不能被拷贝。这是为了防止在多线程环境中出现意外的行为。如果你能够拷贝一个std::atomic对象,那么你可能会在不同的线程中操作同一个std::atomic对象的不同副本,这可能会导致数据竞争和其他并发问题。
因此,为了避免这种情况,std::atomic类的拷贝构造函数被删除,这意味着你不能拷贝std::atomic对象。这也是为什么你不能在类的成员变量声明处直接初始化std::atomic对象,因为这实际上是在尝试拷贝一个std::atomic对象。
示例
以下是一个使用std::atomic的复杂示例,展示了所有的成员函数和非成员函数的使用:
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> atomicInt(0); // 使用原子整型,初始值为0
void increment() {
for (int i = 0; i < 100000; ++i) {
atomicInt++; // 使用原子操作进行自增
}
}
void complexOperations() {
int expected = 0;
while (!atomicInt.compare_exchange_weak(expected, 100)) {
// compare_exchange_weak尝试将atomicInt的值设为100,如果当前值等于expected(0),则设为100并返回true,否则将当前值赋给expected并返回false
expected = 0; // 重置expected
}
int oldValue = atomicInt.exchange(50);
// exchange将atomicInt的值设为50,并返回旧的值
atomicInt.store(25);
// store将atomicInt的值设为25
int loadedValue = atomicInt.load();
// load读取并返回atomicInt的值
std::cout << "Old value from exchange: " << oldValue << "\n";
std::cout << "Loaded value: " << loadedValue << "\n";
}
int main() {
std::thread t1(increment);
std::thread t2(complexOperations);
t1.join();
t2.join();
std::cout << "Final value: " << atomicInt << "\n"; // 输出最终的atomicInt值
return 0;
}
在这个示例中,我们创建了一个原子整型atomicInt,并在一个线程中对其进行自增操作。在另一个线程中,我们使用了compare_exchange_weak,exchange,store,和load等成员函数进行复杂的操作。这些操作都是原子的,即在执行过程中不会被其他线程中断。最后,我们输出了atomicInt的最终值。
这个示例展示了如何在多线程环境中使用std::atomic进行原子操作,以避免数据竞争问题。
std::atomic类的构造情况
std::atomic类提供了一系列的构造函数,用于创建和初始化原子对象。
默认构造函数
默认构造函数创建一个std::atomic对象,但不进行初始化。这意味着,除非显式地使用store或operator=进行初始化,否则该对象的值是未定义的。
std::atomic<int> a; // a的值是未定义的
拷贝构造函数
std::atomic类的拷贝构造函数被删除,这意味着不能通过拷贝构造函数创建std::atomic对象。这是因为,原子操作的语义要求每个std::atomic对象都是唯一的。
std::atomic<int> a(0);
std::atomic<int> b(a); // 错误:拷贝构造函数被删除
移动构造函数
与拷贝构造函数一样,std::atomic类的移动构造函数也被删除。
std::atomic<int> a(0);
std::atomic<int> b(std::move(a)); // 错误:移动构造函数被删除
其他构造函数
std::atomic类还提供了一个构造函数,用于创建并初始化std::atomic对象。
std::atomic<int> a(0); // 创建并初始化a为0
在英语口语交流中,我们通常会这样描述std::atomic的构造函数:“The std::atomic class provides a default constructor for creating an atomic object without initialization. It also provides a constructor for creating and initializing an atomic object. However, the copy constructor and move constructor are deleted, which means each std::atomic object must be unique.” (std::atomic类提供了一个默认构造函数,用于创建但不初始化原子对象。它还提供了一个构造函数,用于创建并初始化原子对象。然而,拷贝构造函数和移动构造函数被删除,这意味着每个std::atomic对象必须是唯一的。)
std::atomic类的使用场景 (Usage Scenarios of std::atomic)
在C++中,std::atomic类被广泛用于多线程编程,特别是在需要进行原子操作的情况下。以下是std::atomic类的一些主要使用场景。
多线程同步 (Multithreading Synchronization)
在多线程环境中,我们经常需要确保对共享数据的访问是原子的,也就是说,在任何时刻,只有一个线程可以访问特定的数据。这就是std::atomic类的主要用途。例如,我们可以使用std::atomic来实现一个线程安全的计数器。
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 初始化一个原子整数
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter; // 原子操作
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(increment)); // 创建10个线程,每个线程都会调用increment函数
}
for (auto& thread : threads) {
thread.join(); // 等待所有线程完成
}
std::cout << "Counter = " << counter << std::endl; // 输出结果应该是10000
return 0;
}
在这个例子中,我们创建了10个线程,每个线程都会调用increment函数,该函数会对counter变量进行1000次增加操作。由于counter是一个std::atomic类型的变量,所以这些操作是原子的,也就是说,在任何时刻,只有一个线程可以对counter进行增加操作。因此,当所有线程都完成后,counter的值应该是10000。
内存模型 (Memory Model)
std::atomic类还可以用于实现复杂的内存模型,例如“释放-获取”模型(“release-acquire” model)。在这种模型中,一个线程可以在一个std::atomic变量上执行一个“释放”操作(“release” operation),然后另一个线程可以在同一个std::atomic变量上执行一个“获取”操作(“acquire” operation)。这可以确保在“释放”操作之前的所有写操作都在“获取”操作之后的读操作之前完成,从而实现线程间的同步。
#include <atomic>
#include <thread>
std::atomic<bool> ready(false);
std::atomic<int> data(0);
void producer() {
data.store(42, std::memory_order_release); // 释放操作
ready.store(true, std::memory_order_release); // 释放操作
}
void consumer() {
while (!ready.load(std::memory_order_acquire));
// 获取操作,等待ready变为true
int result = data.load(std::memory_order_acquire); // 获取操作
std::cout << "The answer is " << result << std::endl; // 输出结果应该是42
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
在这个例子中,producer线程首先将42存储到data变量中,然后将ready变量设置为true。这两个操作都是“释放”操作。然后,consumer线程在一个循环中等待ready变量变为true。当ready变为true时,它会从data变量中读取数据。这两个操作都是“获取”操作。由于“释放-获取”模型的特性,我们可以确保当consumer线程读取data变量时,它读取的是producer线程存储的值,而不是任何旧的或未初始化的值。
其他使用场景 (Other Use Cases)
std::atomic类的应用并不仅限于上述的多线程同步和内存模型,它还可以用于实现更为复杂的并发算法和数据结构。以下是一些更高级的使用场景。
无锁数据结构 (Lock-Free Data Structures)
无锁数据结构是一种特殊的数据结构,它们在设计和实现上能够避免使用互斥锁(mutexes)或其他形式的锁。这些数据结构通常依赖于原子操作来保证线程安全,因此std::atomic类在这里发挥了关键作用。
例如,我们可以使用std::atomic来实现一个无锁的栈。在这个栈中,push和pop操作都是原子的,因此可以在多线程环境中安全地使用。
template <typename T>
class lock_free_stack {
private:
struct node {
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
};
std::atomic<node*> head;
public:
void push(const T& data) {
node* new_node = new node(data);
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next, new_node));
}
std::optional<T> pop() {
node* old_head = head.load();
while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
if (old_head) {
std::optional<T> res = old_head->data;
delete old_head;
return res;
} else {
return std::nullopt;
}
}
};
在这个例子中,我们使用std::atomic<node*>
来表示栈的头部。push操作创建一个新的节点,并使用compare_exchange_weak函数来尝试将其设置为新的头部。pop操作则尝试移除头部的节点。这两个操作都是原子的,因此这个栈是线程安全的。
原子指针 (Atomic Pointers)
std::atomic类也可以用于实现原子指针。原子指针是一种特殊的指针,它的所有操作都是原子的。这对于实现某些高级的并发算法非常有用。
例如,我们可以使用std::atomic<void*>
来实现一个原子的指针交换操作:
std::atomic<void*> ptr1;
std::atomic<void*> ptr2;
void swap_ptrs() {
void* tmp = ptr1.load();
while (!ptr1.compare_exchange_weak(tmp, ptr2.load()));
ptr2.store(tmp);
}
在这个例子中,swap_ptrs函数尝试交换ptr1和ptr2的值。这个操作是原子的,因此可以在多线程环境中安全地使用。
以上就是std::atomic类的一些高级使用场景。需要注意的是,这些高级话题通常需要深入理解并发编程和内存模型,因此在实际编程中,我们应该根据自己的经验和需求来选择合适的工具和技术。std::atomic类是C++提供的一个强大的工具,它可以帮助我们更有效地处理多线程编程中的各种问题。
使用std::atomic类需要注意的点
在使用std::atomic类时,我们需要注意以下几个关键点。这些注意事项将帮助我们更好地理解和使用这个类。
数据类型限制
std::atomic类模板可以用于任何TriviallyCopyable类型(简单复制类型)。但是,对于非整数类型,只有部分操作是原子的。例如,对于std::atomic或std::atomic,只有load()和store()等操作是原子的,而对于std::atomic或std::atomic等整数类型,所有操作都是原子的。
在口语交流中,我们可以这样描述这个问题:“The std::atomic template can be used with any TriviallyCopyable types. However, for non-integer types, only some operations are atomic."(std::atomic模板可以用于任何简单复制类型。但是,对于非整数类型,只有部分操作是原子的。)
原子操作的性能考虑
虽然std::atomic提供了一种线程安全的方式来操作数据,但是原子操作通常比非原子操作要慢。这是因为原子操作需要确保在多线程环境中的一致性,这通常需要额外的处理器指令。因此,在设计并发代码时,我们需要权衡原子操作的线程安全性和性能开销。
在口语交流中,我们可以这样描述这个问题:“Although std::atomic provides a thread-safe way to manipulate data, atomic operations are usually slower than non-atomic operations. This is because atomic operations need to ensure consistency in a multithreaded environment, which usually requires additional processor instructions."(虽然std::atomic提供了一种线程安全的方式来操作数据,但是原子操作通常比非原子操作要慢。这是因为原子操作需要确保在多线程环境中的一致性,这通常需要额外的处理器指令。)
其他注意事项
在使用std::atomic时,我们还需要注意以下几点:
std::atomic不支持复制构造和复制赋值,这是因为这些操作无法保证原子性。在口语交流中,我们可以这样描述这个问题:“std::atomic does not support copy construction and copy assignment, because these operations cannot guarantee atomicity."(std::atomic不支持复制构造和复制赋值,因为这些操作无法保证原子性。)
std::atomic的成员函数是线程安全的,但是如果你在多个线程中同时调用同一个std::atomic对象的成员函数,那么这些调用之间的顺序是未定义的。在口语交流中,我们可以这样描述这个问题:“The member functions of std::atomic are thread-safe, but if you call a member function of the same std::atomic object in multiple threads at the same time, the order of these calls is undefined."(std::atomic的成员函数是线程安全的,但是如果你在多个线程中同时调用同一个std::atomic对象的成员函数,那么这些调用之间的顺序是未定义的。)
对于std::atomic的复合赋值操作(如+=,-=等),我们需要注意这些操作是原子的,但是对于相同的std::atomic对象,不同线程中的复合赋值操作的顺序是未定义的。在口语交流中,我们可以这样描述这个问题:“For the compound assignment operations of std::atomic (such as +=, -=, etc.), these operations are atomic, but the order of compound assignment operations on the same std::atomic object in different threads is undefined."(对于std::atomic的复合赋值操作(如+=,-=等),这些操作是原子的,但是对于相同的std::atomic对象,不同线程中的复合赋值操作的顺序是未定义的。)
以上就是使用std::atomic类需要注意的一些关键点。在编写并发代码时,我们需要充分理解和考虑这些问题,以确保代码的正确性和性能。
为什么要使用std::atomic类
在C++中,std::atomic类(原子类)是一个模板类,用于保证对某种类型的对象进行的操作是原子的,即这些操作在多线程环境下是线程安全的。在多线程编程中,原子操作是非常重要的概念,它可以避免数据竞争(Data Race)和其他并发问题。
解决数据竞争问题
数据竞争(Data Race)是并发编程中的一个常见问题,当两个或更多的线程在没有同步的情况下访问某些数据,并且至少有一个线程对数据进行写操作时,就会发生数据竞争。数据竞争会导致程序的行为变得不可预测和难以复现,这在实际的软件开发中是非常危险的。
在C++中,我们可以使用std::atomic类来避免数据竞争。std::atomic类提供了一种机制,可以确保对某种类型的对象进行的操作是原子的,即这些操作在多线程环境下是线程安全的。这意味着,当一个线程正在对一个std::atomic对象进行操作时,其他线程不能同时对该对象进行操作。这样,我们就可以避免数据竞争。
例如,我们可以使用std::atomic类来实现一个线程安全的计数器:
#include <atomic>
#include <thread>
std::atomic<int> counter(0); // 初始化一个原子整数,初始值为0
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 对原子整数进行自增操作
}
}
int main() {
std::thread t1(increment); // 创建并启动两个线程,分别执行increment函数
std::thread t2(increment);
t1.join(); // 等待两个线程结束
t2.join();
std::cout << counter << std::endl; // 输出结果应该为200000
return 0;
}
在这个例子中,我们使用std::atomic来声明一个原子整数counter,然后在两个线程中对这个原子整数进行自增操作。由于std::atomic保证了自增操作是原子的,所以我们可以确保最后的结果是正确的,即使在多线程环境下。
提高并发性能
除了避免数据竞争,std::atomic类还可以用来提高并发性能。在多线程编程中,我们通常使用锁(例如std::mutex)来保证数据的一致性
和线程安全。然而,锁的使用会带来一定的性能开销,因为它会阻塞线程的执行,直到锁被释放。相比之下,std::atomic类提供的原子操作通常更高效,因为它们不需要阻塞线程的执行。
例如,考虑以下使用锁的代码:
#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter << std::endl;
return 0;
}
在这个例子中,我们使用std::mutex来保护counter,以确保在多线程环境下对counter的操作是线程安全的。然而,每次对counter进行操作时,我们都需要获取和释放锁,这会带来一定的性能开销。
相比之下,如果我们使用std::atomic类,就可以避免这种性能开销:
#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter << std::endl;
return 0;
}
在这个例子中,我们使用std::atomic来声明一个原子整数counter,然后在两个线程中对这个原子整数进行自增操作。由于std::atomic保证了自增操作是原子的,所以我们不需要使用锁,从而可以避免锁带来的性能开销。
总的来说,std::atomic类提供了一种高效的方式来实现线程安全的操作,这对于提高并发性能是非常有用的。
其他理由
除了上述的原因,使用std::atomic类还有其他的理由。例如,std::atomic类提供了一种简单的方式来实现复杂的并发算法,例如无锁数据结构和原子操作。此外,std::atomic类还提供了一种方式来实现低级别的同步和通信,这对于某些特定的应用场景是非常有用的。
总的来说,std::atomic类是C++中实现并发编程的一个重要工具,它提供了一种高效、安全的方式来实现原子操作。无论是在解决数据竞争问题,还是在提高并发性能,甚至在实现复杂的并发算法和
低级别的同步和通信,std::atomic类都能发挥重要的作用。
例如,我们可以使用std::atomic_flag来实现一个简单的自旋锁(Spinlock):
#include <atomic>
#include <thread>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)) // acquire lock
; // spin
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // release lock
}
}
int main()
{
std::thread t1(f, 1);
std::thread t2(f, 2);
t1.join();
t2.join();
return 0;
}
在这个例子中,我们使用std::atomic_flag来实现一个自旋锁。当一个线程需要获取锁时,它会不断地尝试设置std::atomic_flag,直到成功为止。当一个线程需要释放锁时,它会清除std::atomic_flag。这是一个非常简单的例子,但是它展示了std::atomic类在实现复杂的并发算法中的作用。
总的来说,std::atomic类是C++中实现并发编程的一个重要工具,它提供了一种高效、安全的方式来实现原子操作。无论是在解决数据竞争问题,还是在提高并发性能,甚至在实现复杂的并发算法和低级别的同步和通信,std::atomic类都能发挥重要的作用。