C++ std::atomic 类型详解

1. 简介

std::atomic 是 C++ 提供的一种用于多线程编程的同步原语,保证对数据类型的操作是原子的,即不会被中断或与其他线程产生冲突。它适用于需要在线程之间共享数据而不需要加锁的场景,提供比传统的锁(如 std::mutex)更好的性能。

2. 常用场景

std::atomic 通常用于以下情况:

  • 共享资源的安全访问:在多线程程序中,多个线程需要访问和修改同一变量。
  • 避免锁机制的开销:锁会带来性能上的开销,而 std::atomic 提供了更轻量的原子操作。
  • 简单的同步:当仅需要单个变量的同步时,使用 std::atomic 是非常高效的。
3. 常用原子操作

std::atomic 支持多种原子操作,包括:

  • 加载 (load)
  • 存储 (store)
  • 交换 (exchange)
  • 比较并交换 (compare_exchange_strong, compare_exchange_weak)
  • 增量和减量 (fetch_add, fetch_sub)
4. std::atomic 支持的类型
  • 内置基础类型:如 int, bool, char, float, double 等。
  • 自定义类型:可以将自定义结构体/类声明为 std::atomic<T>,但必须保证其满足 "trivially copyable"(平凡可拷贝)要求,即没有自定义构造、析构、拷贝构造和赋值运算符等。
5. 基本用法示例
5.1 基础操作

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;  // 原子加操作
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;  // 输出 2000
    return 0;
}

解释

  • std::atomic<int> counter(0):声明了一个初始值为 0 的原子整型变量 counter
  • ++counter:原子自增操作,确保多个线程同时访问时不会发生竞态条件。
5.2 compare_exchange 操作

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> value(10);

void compareAndSwap(int expected, int desired) {
    if (value.compare_exchange_strong(expected, desired)) {
        std::cout << "Swap successful! New value: " << value << std::endl;
    } else {
        std::cout << "Swap failed. Expected: " << expected << ", actual: " << value << std::endl;
    }
}

int main() {
    std::thread t1(compareAndSwap, 10, 20);
    std::thread t2(compareAndSwap, 10, 30);

    t1.join();
    t2.join();

    return 0;
}

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> value(10);

void compareAndSwap(int expected, int desired) {
    if (value.compare_exchange_strong(expected, desired)) {
        std::cout << "Swap successful! New value: " << value << std::endl;
    }
    else {
        std::cout << "Swap failed. Expected: " << expected << ", actual: " << value << std::endl;
    }
}

int main() {
    std::thread t1(compareAndSwap, 10, 20);
    std::thread t2(compareAndSwap, 10, 30);

    t1.join();
    t2.join();

    return 0;
}
 

  • compare_exchange_strong 尝试将 value 的值从 expected 修改为 desired。如果 value 等于 expected,修改成功;否则修改失败。
  • compare_exchange_strong 是强比较交换,失败率较低。compare_exchange_weak 是弱比较交换,适合用于循环中。
6. 注意事项和常见坑
6.1 内存顺序(Memory Order)

std::atomic 默认使用顺序一致性(sequential consistency)的内存模型,即所有线程都能以相同的顺序观察到所有原子操作。可以通过传入 memory_order 来调整内存顺序,常见的有:

  • memory_order_relaxed:不保证任何顺序,仅保证原子性。适合不依赖顺序的场景。
  • memory_order_acquire:保证在获取数据后,之前的所有写操作对当前线程可见。
  • memory_order_release:保证在释放数据前,当前线程的写操作对其他线程可见。
  • memory_order_acq_rel:结合了 acquirerelease 的效果。
  • memory_order_seq_cst:默认行为,顺序一致性。

常见坑:不理解内存顺序可能导致错误的线程同步行为。在性能敏感的场合,应根据实际需求选择适合的内存顺序。

6.2 复合操作的竞态条件

虽然 std::atomic 可以避免单个变量的竞态条件,但多个操作的组合仍可能出现竞态条件。例如,以下代码虽然使用了 std::atomic,但仍然存在问题:

std::atomic<int> counter(0);

void update() {
    if (counter.load() == 0) {  // 竞态条件:其他线程可能已经改变了 counter 的值
        counter.store(1);
    }
}

在多线程环境下,loadstore 之间没有原子性保障,可能导致竞态问题。此类场景应使用 compare_exchange 之类的复合原子操作。

6.3 std::atomic_flag

std::atomic_flag 是比 std::atomic 更加基础的原子类型,只支持 clear()test_and_set() 操作,常用于实现锁或自旋锁机制。

#include <atomic>
#include <thread>
#include <iostream>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void lockFunction() {
    while (lock.test_and_set(std::memory_order_acquire)) { // 自旋直到获取锁
        // 等待锁
    }
    // 临界区
    std::cout << "Thread " << std::this_thread::get_id() << " acquired lock.\n";
    lock.clear(std::memory_order_release);  // 释放锁
}

int main() {
    std::thread t1(lockFunction);
    std::thread t2(lockFunction);

    t1.join();
    t2.join();
    return 0;
}
 

解释

  • test_and_set:设置标志并返回其之前的值。如果标志已被设置(锁已被其他线程持有),则自旋等待。
  • clear:清除标志,释放锁。
小结
  • 优点std::atomic 提供轻量、高效的原子操作,适用于不需要复杂锁机制的场景。
  • 注意事项:需理解内存顺序对性能和正确性的影响。对于复杂的多操作组合,仍需谨慎处理竞态条件。
  • 常见坑:不当使用 loadstore 导致竞态条件、对内存顺序的误解、复合操作的不原子性。

8.知识补充

除了基本用法和常见场景之外,std::atomic 还有一些更深入的知识和高级应用场景,涉及到更复杂的并发控制、性能优化、内存模型等方面。以下是一些进一步的知识点和应用技巧:

1. std::atomic 的内存模型

1.1 顺序一致性(Sequential Consistency)

std::atomic 默认采用顺序一致性模型,即所有的原子操作会按照某种顺序执行,所有线程可以看到这些操作发生的顺序是完全一致的。这种模型虽然最直观,但性能相对较低,特别是在多核系统中。

std::atomic<int> x(0);
std::atomic<int> y(0);

// 顺序一致性保证所有线程可以一致地看到操作顺序
x.store(1);  // store 操作
int r1 = y.load();  // load 操作

1.2 更宽松的内存顺序(Relaxed Memory Order)

可以通过指定内存顺序来优化性能,但会引入更复杂的并发问题。以下是几种常见的内存顺序:

compare_exchange 操作的内存顺序较为复杂,可以为成功和失败情况分别指定不同的内存顺序。例如:

1.3 compare_exchange 中的内存顺序

  • memory_order_relaxed:只保证操作是原子的,不保证操作顺序。适合一些不依赖其他内存操作顺序的场景,比如计数器递增。
  • std::atomic<int> counter(0);

    void increment() {
        counter.fetch_add(1, std::memory_order_relaxed);  // 仅保证原子性,不保证顺序
    }

  • memory_order_acquire:当前操作与之前的操作存在依赖关系,保证后续读取到的是最新值。

  • std::atomic<int> flag(0);

    void consumer() {
        while (!flag.load(std::memory_order_acquire)) {  // 依赖之前的写入操作,保证读取最新值
            // 自旋等待
        }
        // 进入临界区
    }

  • memory_order_release:保证之前的操作在此之前完成,适合发布事件或信号的场景。

  • void producer() {
        // 临界区逻辑
        flag.store(1, std::memory_order_release);  // 发布信号
    }

  • memory_order_acq_rel:结合了 acquirerelease 的语义,适合需要在发布前进行依赖检查的场景。

  • memory_order_consume:类似于 acquire,但仅依赖于使用的操作,而非所有后续操作。它可以提高性能,但其实现和保证依赖编译器。

std::atomic<int> value(0);
int expected = 0;

value.compare_exchange_strong(expected, 1, std::memory_order_acq_rel, std::memory_order_relaxed);

  • 成功时采用 acq_rel,失败时采用 relaxed。这样在不需要成功时的严格顺序控制时,失败的情况下可以更宽松,提升性能。

2. std::atomicvolatile 的区别

许多人常常混淆 std::atomicvolatile。二者在多线程中的作用非常不同:

  • volatile 只是告诉编译器不要对其进行优化,适用于处理与硬件相关的操作,无法保证线程安全。
  • std::atomic 是多线程同步原语,保证原子性和线程安全,适合多线程编程。
  • volatile int counter = 0;  // 不安全,不能用于多线程
    std::atomic<int> atomic_counter = 0;  // 线程安全

3. std::atomic 的优化与陷阱

3.1 自旋锁与 CPU 过度占用

在高并发的场景下,使用自旋锁时可能导致 CPU 资源的浪费,尤其是在资源紧张的情况下。可以通过结合自旋锁与 std::this_thread::yield() 来减少 CPU 过度占用。

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void spinlock_acquire() {
    while (lock.test_and_set(std::memory_order_acquire)) {
        std::this_thread::yield();  // 如果锁被占用,主动放弃 CPU 资源
    }
}

void spinlock_release() {
    lock.clear(std::memory_order_release);
}

3.2 ABA 问题

std::atomiccompare_exchange 操作可能遇到 ABA 问题。即一个线程看到变量从 A 变成 B 再变回 A,误以为变量从未被修改过。这在 std::atomic 的 CAS 操作中可能导致数据竞争。解决 ABA 问题的一种方法是使用指针和版本号结合,或使用更高级的 std::atomic<std::shared_ptr> 等技术。

3.3 多线程的伪共享(False Sharing)

伪共享是指多个线程操作不同的 std::atomic 变量,但这些变量位于同一个缓存行(通常是 64 字节),导致缓存一致性协议频繁地刷新和更新缓存行,降低性能。

优化技巧: 可以通过将变量按 64 字节对齐或使用 alignas 关键字避免伪共享。

struct alignas(64) PaddedAtomic {
    std::atomic<int> value;
};

4. std::atomic 和非阻塞数据结构

4.1 原子队列(Atomic Queue)

std::atomic 可以用来实现无锁队列、栈等数据结构,避免传统锁带来的开销。无锁数据结构在高并发下表现尤为出色,减少了上下文切换和锁竞争带来的性能损耗。

一个简单的无锁栈例子:

#include <atomic>

struct Node {
    int data;
    Node* next;
};

std::atomic<Node*> head(nullptr);

void push(int value) {
    Node* newNode = new Node{value, nullptr};
    newNode->next = head.load();
    while (!head.compare_exchange_weak(newNode->next, newNode)) {
        // CAS 失败时重新尝试
    }
}

int pop() {
    Node* oldHead = head.load();
    while (oldHead && !head.compare_exchange_weak(oldHead, oldHead->next)) {
        // CAS 失败时重新尝试
    }
    if (oldHead) {
        int value = oldHead->data;
        delete oldHead;
        return value;
    }
    return -1;  // 空栈
}
 

4.2 原子共享指针(Atomic Shared Pointers)

C++11 中,std::atomic 还支持 std::shared_ptr,用于管理对象的引用计数。这种技术特别适合实现并发的数据结构,避免对象的过早销毁或误用。

#include <memory>
#include <atomic>

std::atomic<std::shared_ptr<int>> atomicPtr;

void foo() {
    auto localPtr = std::make_shared<int>(42);
    atomicPtr.store(localPtr);
}

void bar() {
    auto loadedPtr = atomicPtr.load();
    if (loadedPtr) {
        std::cout << *loadedPtr << std::endl;
    }
}

5. std::atomic 和线程安全单例模式

std::atomic 可以用于实现线程安全的单例模式:

#include <atomic>

class Singleton {
public:
    static Singleton* getInstance() {
        Singleton* temp = instance.load(std::memory_order_acquire);
        if (!temp) {
            std::lock_guard<std::mutex> lock(mtx);
            temp = instance.load(std::memory_order_relaxed);
            if (!temp) {
                temp = new Singleton();
                instance.store(temp, std::memory_order_release);
            }
        }
        return temp;
    }

private:
    Singleton() = default;
    static std::atomic<Singleton*> instance;
    static std::mutex mtx;
};

std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;

总结

  • std::atomic 是 C++ 中强大的并发工具,适用于避免锁机制带来的性能损耗。
  • 需要理解内存顺序模型以及如何选择适合的内存顺序以提高性能。
  • 在高并发场景中可能遇到 ABA 问题、伪共享等高级问题,需根据具体需求进行优化。
  • 可以结合 std::atomic 实现无锁数据结构、线程安全的单例模式和原子引用计数等复杂应用。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值