专业的编程技术博客社区

网站首页 > 博客文章 正文

【C++并发编程】(五)原子操作(并发原子类)

baijin 2024-09-26 06:58:42 博客文章 3 ℃ 0 评论

(五)原子操作

原子操作(Atomic Operations)是一种在并发编程中用于防止数据竞争和保证线程安全性的机制。原子操作是不可中断的操作,一旦开始,就必须完全执行完毕,不受其他线程干扰,可以确保多个线程在访问和修改共享数据时不会发生冲突,从而避免了数据竞争和相关的并发问题。

std::atomic是C++11标准库引入的一个模板类,它提供了对基本数据类型(如int、bool、float,指针等)的原子操作支持。

C++中的原子操作

在C++中,std::atomic类型提供了一系列操作保证了操作的原子性,即操作在执行过程中不会被其他线程打断。

  1. 构造和赋值
  • 构造函数:可以用初始值构造std::atomic对象。
  • 赋值操作符:= 可以用于给std::atomic对象赋值,但这不是原子操作。
  1. 存储和加载
  • store(T desr, std::memory_order order = std::memory_order_seq_cst):原子地将指定的值存储到原子对象中。
  • load(std::memory_order order = std::memory_order_seq_cst) const:原子地从原子对象中加载值。
  1. 算术操作
  • 对于整数类型的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
  1. 比较并交换
  • 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 并执行交换。

伪失败指的是某些操作在逻辑上应该成功但由于某些原因(如硬件或操作系统的优化)而未能成功。

  1. 交换
  • exchange(T desired, std::memory_order order = std::memory_order_seq_cst):原子地将desired与当前值交换,并返回旧值。
  1. 位操作
  • 对于整数类型的std::atomic,也提供了位操作符的重载,如&=、|=、^=等。
  1. 非成员函数
  • 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操作是原子的,即使在多线程环境下也不会产生数据竞争。

原子操作与互斥锁

原子操作通常比互斥锁具有更高的性能,但原子操作通常针对单个数据项(如一个整数或指针)进行,它们确保了对该数据项的访问是原子的,即不会被其他线程打断。互斥锁则通常用于保护一个代码块或整个数据结构,确保在该代码块或数据结构被访问时,只有一个线程能够执行。因此,在选择使用原子操作还是互斥锁时,需要根据具体的应用场景和需求进行权衡。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表