C++的原子变量作用

发布于:2024-04-28 ⋅ 阅读:(148) ⋅ 点赞:(0)

C++11中引入的std::atomic<T>是一个模板类,它允许对特定类型的变量进行原子操作。原子操作是不可中断的操作,即在执行过程中不会被其他线程或CPU核心打断。这对于多线程编程至关重要,因为它确保了操作的原子性,从而避免了数据竞争和不一致的问题。

一、原子变量的作用

原子变量在多线程环境中特别有用,因为它们提供了一种无需使用互斥锁(mutexes)或信号量(semaphores)等同步机制就能安全地访问和修改共享数据的方式。互斥锁虽然也能保证数据的安全性,但它们通常涉及到更多的开销,比如线程的阻塞和唤醒,这可能导致性能下降。

std::atomic<T>支持的类型主要包括整型(如int、long等)、指针类型,以及C++17后支持的自定义类型(需满足一定的条件)。不支持的类型包括浮点型和复合类型(如结构体或类)。

原子变量提供了一系列成员函数,用于执行原子操作,如load(加载值)、store(存储值)、exchange(交换值)、compare_exchange_strong和compare_exchange_weak(比较并交换值,若值相等则交换新值)等。这些操作都是原子的,即它们要么完全执行,要么完全不执行,不会被其他线程打断。

下面是一个简单的示例,展示了如何在C++中使用原子变量:

#include <iostream>  
#include <thread>  
#include <atomic>  
#include <vector>  
  
std::atomic<int> counter(0); // 初始化一个原子整型变量counter,初始值为0  
  
void increment(int id) {  
    for (int i = 0; i < 1000; ++i) {  
        ++counter; // 对counter进行原子自增操作  
    }  
    std::cout << "Thread " << id << " finished. Counter: " << counter << std::endl;  
}  
  
int main() {  
    std::vector<std::thread> threads;  
    for (int i = 0; i < 10; ++i) {  
        threads.emplace_back(increment, i); // 创建并启动10个线程,每个线程都会增加counter的值  
    }  
    for (auto& t : threads) {  
        t.join(); // 等待所有线程完成  
    }  
    std::cout << "Final counter value: " << counter << std::endl; // 输出最终的counter值,应该是10000(每个线程增加1000次)  
    return 0;  
}

在这个示例中,我们创建了一个原子整型变量counter,并在多个线程中对其进行自增操作。由于使用了原子变量,我们不需要额外的同步机制来确保counter的值在多线程环境下的正确性。最终,所有线程完成后,counter的值应该是10000(每个线程增加1000次)。

二、原子变量成员函数

下面详细解释std::atomic类型提供的几个主要成员函数,这些函数用于执行原子操作。

1、load 函数

load 函数用于原子地加载原子变量的值。在多线程环境中,直接读取原子变量的值可能会导致数据竞争,因为其他线程可能在读取过程中修改了该变量的值。使用 load 函数可以确保在读取过程中,变量的值不会被其他线程修改。

T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;

load 函数接受一个 std::memory_order 参数,用于指定内存顺序。std::memory_order_seq_cst 是默认的顺序,它提供了最强的顺序保证,即所有线程看到的操作顺序都是一致的。其他可选的内存顺序提供了更弱的保证,可以在某些情况下提高性能。

2、store 函数

store 函数用于原子地存储一个值到原子变量中。与直接赋值相比,使用 store 函数可以避免数据竞争。

void store(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;

同样,store 函数也接受一个 std::memory_order 参数来指定内存顺序。

3、exchange 函数

exchange 函数用于原子地交换原子变量的当前值和一个新值,并返回原子变量之前的值。

T exchange(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;

这个函数通常用于实现一些需要基于当前值进行计算的原子操作,比如原子地增加或减少一个计数器的值。

4、compare_exchange_strong 和 compare_exchange_weak 函数

这两个函数用于实现“比较并交换”操作,它们会先比较原子变量的当前值是否等于一个预期值,如果相等,则交换为新值。它们之间的主要区别在于失败时的行为:compare_exchange_strong 在失败时总是返回 false,而 compare_exchange_weak 在失败时可能返回 true 或 false,这取决于具体的实现和硬件支持。

bool compare_exchange_strong(T& expected, T val,  
                              std::memory_order success,  
                              std::memory_order failure) noexcept;  
bool compare_exchange_weak(T& expected, T val,  
                           std::memory_order success,  
                           std::memory_order failure) noexcept;

这两个函数都接受两个内存顺序参数:success 用于指定操作成功时的内存顺序,failure 用于指定操作失败时的内存顺序。

expected 参数是一个引用,它指向一个变量,该变量在调用时包含预期值,并在调用后包含实际值(如果比较失败)。val 参数是新值,如果比较成功,则原子变量将被设置为这个值。

比较并交换操作在多线程编程中非常有用,特别是用于实现无锁数据结构或实现复杂的原子操作。

5、关于内存顺序

在上面的函数中,std::memory_order 参数用于指定内存操作的顺序。不同的内存顺序提供了不同的性能和一致性保证。选择正确的内存顺序是编写高效且正确的多线程代码的关键。常见的 std::memory_order 值包括:

std::memory_order_relaxed:最弱的顺序保证,只保证原子操作的原子性。
std::memory_order_consume:允许依赖关系在数据依赖上传播,但不保证其他线程看到的顺序。
std::memory_order_acquire:确保在读取操作之前的所有读取和写入操作都不会被重排序到读取操作之后。
std::memory_order_release:确保在写入操作之后的所有读取和写入操作都不会被重排序到写入操作之前。
std::memory_order_acq_rel:结合了 acquire 和 release 的顺序保证。
std::memory_order_seq_cst:最强的顺序保证,它确保所有线程看到的操作顺序是一致的。
正确选择内存顺序需要根据具体的应用场景和性能需求来决定。在大多数情况下,std::memory_order_seq_cst 是最安全的选择,但它可能会引入不必要的性能开销。在更复杂的场景下,可能需要更细致地控制内存顺序来优化性能。


网站公告

今日签到

点亮在社区的每一天
去签到