在并发环境中,如何保证数据在多个线程之间的同步与一致性是一个关键问题。传统的同步方式使用互斥锁(mutex)来保护共享数据,但锁的使用带来了额外的开销。为了解决这个问题,C++11 提供了 atomic<T> 模板类,它允许在不使用锁的情况下安全地进行多线程编程。atomic<T> 提供了对基础数据类型的原子操作支持,能够有效避免竞争条件,提升并发性能。


一、什么是 atomic<T>

atomic<T> 是 C++11 提供的一个模板类,用于在多线程环境中安全地操作基础数据类型。它通过底层 CPU 提供的指令集,保证了对变量的操作是原子性的,即每个操作要么完整地执行,要么根本不执行,不会因为线程切换导致数据不一致。

支持的类型

atomic<T> 模板类支持以下类型:

  • 基础整型(如 intlongunsigned int
  • 布尔类型(bool
  • 指针类型

值得注意的是,atomic<T> 不支持浮点类型和自定义数据类型


二、atomic<T> 的常用操作

atomic<T> 提供了一组线程安全的操作,用于读取、修改、比较交换等。其主要操作函数包括:

  1. 构造函数

    • atomic() noexcept:默认构造函数,初始化一个原子变量。
    • atomic(T val) noexcept:使用初始值 val 初始化原子变量。
    • atomic(const atomic&) = delete:禁用拷贝构造函数,防止原子变量被复制。
  2. 赋值操作

    • atomic& operator=(const atomic&) = delete:禁用赋值操作,防止赋值导致的竞争问题。
  3. 常用方法

    • void store(const T val) noexcept:存储一个值 val 到原子变量。
    • T load() noexcept:读取原子变量的值。
    • T fetch_add(const T val) noexcept:将原子变量的值与 val 相加,并返回原值。
    • T fetch_sub(const T val) noexcept:将原子变量的值减去 val,并返回原值。
    • T exchange(const T val) noexcept:将原子变量的值替换为 val,并返回原值。
    • bool compare_exchange_strong(T &expect, const T val) noexcept:比较原子变量的值与 expect,如果相等则将其替换为 val,并返回 true;否则将原子变量的值更新为 expect,并返回 false
  4. 性能查询

    • bool is_lock_free():查询某原子类型的操作是否是无锁的。如果返回 true,表示操作由 CPU 原生指令直接支持;如果返回 false,表示编译器使用了内部的锁机制来保证操作的安全性。

三、原子操作示例

以下是一些 atomic<T> 操作的示例,展示了如何在多线程环境中安全地使用原子变量。

示例 1:基本操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <atomic> // 引入原子类型头文件

int main() {
    std::atomic<int> a = 3; // 初始化原子变量 a
    std::cout << "a = " << a.load() << std::endl; // 输出当前值:3

    a.store(8); // 存储新值 8
    std::cout << "a = " << a.load() << std::endl; // 输出更新后的值:8

    int old = a.fetch_add(5); // a 的值加 5,返回原值
    std::cout << "old = " << old << ",a = " << a.load() << std::endl; // 输出:old = 8,a = 13

    old = a.fetch_sub(2); // a 的值减去 2,返回原值
    std::cout << "old = " << old << ",a = " << a.load() << std::endl; // 输出:old = 13,a = 11
}

示例 2:比较交换(CAS)

CAS(Compare-And-Swap)是实现无锁算法的基础操作。它允许我们在读取原子变量时,检查其当前值是否与预期值匹配,如果匹配则进行更新。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <atomic>

int main() {
    std::atomic<int> ii = 3;  // 原子变量
    int expect = 4;           // 预期值
    int val = 5;              // 新的值

    // 比较原子变量 ii 的值与 expect
    // 如果相等则将其替换为 val
    bool bret = ii.compare_exchange_strong(expect, val);

    std::cout << "bret = " << bret << std::endl; // 输出比较结果,false 因为 ii != expect
    std::cout << "ii = " << ii << std::endl;     // 输出原子变量的当前值
    std::cout << "expect = " << expect << std::endl; // 输出 expect 被更新为 ii 的值
}

在这个例子中,compare_exchange_strong() 尝试将 ii 的值从 3 更新为 5,但由于 expect4,因此操作失败,ii 保持原值 3,同时 expect 被更新为 ii 的当前值。


四、使用场景

4.1 计数器

原子整型可以用作计数器,在并发环境下安全地递增或递减。

1
2
3
4
5
std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1); // 线程安全的递增操作
}

4.2 布尔开关

原子布尔型可以用作线程间的信号传递,用来实现轻量级的开关机制。

1
2
3
4
5
6
7
8
std::atomic<bool> flag(false);

void worker_thread() {
    while (!flag.load()) {
        // 等待 flag 被设置为 true
    }
    // 开始执行任务
}

4.3 无锁队列

原子操作是实现无锁数据结构的基础,CAS 指令可以保证数据结构的线程安全性,而无需使用互斥锁。无锁队列广泛应用于高性能、多线程场景中。


总结

atomic<T> 是 C++11 中为多线程编程提供的重要工具。它利用底层硬件的支持,实现了对基础数据类型的无锁访问和修改操作。在适当的场景下使用原子操作,能够避免锁带来的性能开销,并且能够简化代码逻辑。然而,虽然原子操作可以避免锁的使用,但它们也有自己的局限性。