文章目录
- (五)原子操作
- C++中的原子操作
- 内存顺序
- 原子操作解决数据竞争问题
(五)原子操作
原子操作(Atomic Operations)是一种在并发编程中用于防止数据竞争和保证线程安全性的机制。原子操作是不可中断的操作,一旦开始,就必须完全执行完毕,不受其他线程干扰,可以确保多个线程在访问和修改共享数据时不会发生冲突,从而避免了数据竞争和相关的并发问题。
std::atomic
是C++11标准库引入的一个模板类,它提供了对基本数据类型(如int、bool、float,指针等)的原子操作支持。
C++中的原子操作
在C++中,std::atomic
类型提供了一系列操作保证了操作的原子性,即操作在执行过程中不会被其他线程打断。
- 构造和赋值:
- 构造函数:可以用初始值构造
std::atomic
对象。 - 赋值操作符:
=
可以用于给std::atomic
对象赋值,但这不是原子操作。
- 存储和加载:
store(T desr, std::memory_order order = std::memory_order_seq_cst)
:原子地将指定的值存储到原子对象中。load(std::memory_order order = std::memory_order_seq_cst) const
:原子地从原子对象中加载值。
- 算术操作:
- 对于整数类型的
std::atomic
,提供了++
、--
、+=
、-=
、*=
、/=
、&=
、|=
、^=
等算术操作符的重载,它们都是原子的。 fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst)
: 原子地增加arg
fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst)
: 原子地减少arg
- 比较并交换:
compare_exchange_weak(T& expected, T desired, std::memory_order success = std::memory_order_seq_cst, std::memory_order failure = std::memory_order_seq_cst)
compare_exchange_strong(T& expected, T desired, std::memory_order success = std::memory_order_seq_cst, std::memory_order failure = std::memory_order_seq_cst)
这两个函数尝试用desired
替换当前值,但仅当当前值等于expected
时才这样做。如果替换成功,expected
将被更新为新的值。
两者的主要区别在于 compare_exchange_weak
可能会在期望值和当前值匹配时因伪失败而返回 false
,而 compare_exchange_strong
则保证在匹配时一定返回 true
并执行交换。
伪失败指的是某些操作在逻辑上应该成功但由于某些原因(如硬件或操作系统的优化)而未能成功。
- 交换:
exchange(T desired, std::memory_order order = std::memory_order_seq_cst)
:原子地将desired
与当前值交换,并返回旧值。
- 位操作:
- 对于整数类型的
std::atomic
,也提供了位操作符的重载,如&=
、|=
、^=
等。
- 非成员函数:
std::atomic_flag
是一个特殊的原子类型,它只提供了一个操作:test_and_set
,用于实现自旋锁等低级同步原语。
内存顺序
上述许多操作都接受一个std::memory_order
参数来指定内存顺序(Memory Order)。内存顺序决定了两个或多个内存访问操作之间的相对顺序。在多线程环境中,由于处理器缓存、指令重排和编译器优化等因素,内存访问的顺序可能并不总是按照代码中的顺序执行。这可能导致数据竞争(data race)和未定义行为(undefined behavior)。在C++11及以后的版本中,std::atomic
和std::memory_order
一起提供了对内存顺序的细粒度控制。
std::memory_order
枚举类型定义了不同的内存顺序约束,这些约束用于控制编译器和处理器对内存操作的重新排序。不同的内存顺序约束有不同的性能和正确性保证。常见的内存顺序包括:
std::memory_order_relaxed
:最宽松的约束,不保证任何特定的内存顺序。适用于不需要同步或顺序保证的原子操作,如统计计数器的增加。std::memory_order_consume
:主要应用于消费者-生产者模型中的依赖关系同步,确保依赖于原子操作结果的后续操作能按正确顺序执行。std::memory_order_acquire
:在读取原子变量之前使用,确保之前的所有读取和写入操作对当前线程可见,常用于读-写锁(read-write lock)的读操作。std::memory_order_release
:在写入原子变量之后使用,确保之后的所有读取和写入操作对其他线程可见,常用于写-写锁(write-write lock)的写操作或发布新值到共享变量。std::memory_order_acq_rel
:结合了acquire
和release
的语义,常用于保护一段代码(critical section)的起始和结束,确保只有单一线程能执行这段代码。std::memory_order_seq_cst
:提供最强的顺序一致性保证,保证所有线程看到的操作顺序都是一致的,就像单线程执行一样。适用于需要严格顺序保证的场景。
原子操作解决数据竞争问题
【C++并发编程】(三)互斥锁:std::mutex 使用互斥锁解决的数据竞争问题,原子操作也可以解决。下面给出一个例子:
#include <iostream>
#include <thread>
std::atomic<int> counter(0); // 定义一个原子整数counter
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 原子自增操作
}
}
int main() {
std::thread th1(increment);
std::thread th2(increment);
th1.join();
th2.join();
std::cout << "Final counter value: " << counter << std::endl;
// Final counter value: 200000
return 0;
}
由于counter
是std::atomic
类型的,所以++counter
操作是原子的,即使在多线程环境下也不会产生数据竞争。
原子操作与互斥锁
原子操作通常比互斥锁具有更高的性能,但原子操作通常针对单个数据项(如一个整数或指针)进行,它们确保了对该数据项的访问是原子的,即不会被其他线程打断。互斥锁则通常用于保护一个代码块或整个数据结构,确保在该代码块或数据结构被访问时,只有一个线程能够执行。因此,在选择使用原子操作还是互斥锁时,需要根据具体的应用场景和需求进行权衡。