首页 > 后端开发 > C++ > 正文

c++如何使用原子操作atomic_c++多线程原子操作库应用

尼克
发布: 2025-09-23 18:47:01
原创
565人浏览过
C++中std::atomic通过硬件指令实现共享变量的原子操作,避免数据竞争。它比互斥锁更轻量,适用于单变量并发操作,提升性能。支持整型、浮点、指针及满足平凡复制的自定义类型。核心操作包括load/store、fetch_add等读-改-写操作,以及compare_exchange_weak/strong实现无锁同步。内存序(memory order)控制操作的可见性和顺序:relaxed仅保证原子性;acquire/release配对使用,建立线程间happens-before关系;seq_cst为默认最强顺序一致性。实际应用需注意ABA问题(可用版本号规避)、伪共享(通过缓存行对齐)、避免混合原子与非原子访问、谨慎选择内存序以防可见性错误,并用is_lock_free判断是否真正无锁。

c++如何使用原子操作atomic_c++多线程原子操作库应用

C++中利用std::atomic库进行原子操作,核心在于确保多线程环境下对共享变量的读写是不可中断的、原子的,从而避免数据竞争和未定义行为。它提供了比互斥锁更细粒度的同步机制,尤其适用于单个变量的并发操作,能够有效提升性能。

解决方案

在C++多线程编程中,当我们处理共享数据时,std::atomic 提供了一种强大且高效的方式来保证操作的原子性。它通过利用底层硬件指令(如CAS, Compare-And-Swap)或在必要时使用轻量级锁,确保对变量的读、写或读-改-写操作作为一个整体完成,不会被其他线程中断。

要使用std::atomic,你需要包含<atomic>头文件。

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

// 声明一个原子计数器
std::atomic<int> global_counter(0);

void increment_counter() {
    for (int i = 0; i < 100000; ++i) {
        // 使用fetch_add进行原子加操作
        // 这等价于 old_val = global_counter; global_counter = old_val + 1; 并保证整个过程原子性
        global_counter.fetch_add(1); 
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "最终计数器值: " << global_counter.load() << std::endl; // 使用load()原子读取
    // 预期输出是 10 * 100000 = 1000000

    // 也可以直接赋值和读取,它们也是原子操作
    std::atomic<bool> flag(false);
    flag.store(true); // 原子写入
    if (flag.load()) { // 原子读取
        std::cout << "Flag is true." << std::endl;
    }

    // 比较并交换 (CAS) 是原子操作的核心
    std::atomic<int> value(10);
    int expected = 10;
    int desired = 20;
    // 如果value当前是expected,就把它设置为desired,并返回true
    // 否则,不改变value,并把value的当前值赋给expected,返回false
    if (value.compare_exchange_strong(expected, desired)) {
        std::cout << "CAS successful, value is now: " << value.load() << std::endl; // 20
    } else {
        std::cout << "CAS failed, value is still: " << value.load() << ", expected was: " << expected << std::endl;
    }

    expected = 20; // 再次尝试,这次expected是正确的
    desired = 30;
    if (value.compare_exchange_strong(expected, desired)) {
        std::cout << "Another CAS successful, value is now: " << value.load() << std::endl; // 30
    } else {
        std::cout << "Another CAS failed." << std::endl;
    }

    return 0;
}
登录后复制

为什么我们需要原子操作,互斥锁不够吗?

我记得刚开始接触多线程编程时,总觉得一个std::mutex就能解决所有并发问题。毕竟,它能确保任何时刻只有一个线程进入临界区,听起来万无一失。然而,随着项目复杂度的增加,我逐渐意识到,互斥锁虽然强大,但并非总是最优解,甚至在某些场景下会成为性能瓶颈

立即学习C++免费学习笔记(深入)”;

原子操作和互斥锁解决的都是数据竞争问题,但它们的方式和适用场景大相径庭。

互斥锁(std::mutex)的工作原理是,它会锁定一个代码块,确保在任何给定时间只有一个线程可以执行该代码块。这很好,当你需要保护一个复杂的临界区,里面可能包含多个变量的修改、复杂的逻辑判断,或者涉及到I/O操作时,互斥锁是首选。它提供了一种粗粒度的同步,能够有效地管理复杂的共享状态。

原子操作(std::atomic)则不同,它专注于单个变量的操作。例如,对一个整数进行增量操作(i++),看似简单,但在多线程环境下,它实际上是“读取i的值”、“将i的值加1”、“将新值写回i”这三个步骤。如果这三个步骤不是原子的,另一个线程可能在中间读取到一个旧值,或者在写入前覆盖了你的中间结果,导致数据丢失或错误。std::atomic就是为了确保这“读取-修改-写入”的整个过程是不可分割的。

那为什么说互斥锁不够呢?

  1. 性能开销: 互斥锁的开销相对较大。每次加锁和解锁都可能涉及到操作系统层面的上下文切换,这在频繁操作时会带来显著的性能损失。特别是在高并发、临界区很短(比如只是对一个计数器加1)的场景下,互斥锁的同步成本可能远超实际业务逻辑的执行成本。
  2. 粒度问题: 互斥锁是粗粒度的。即使你只是想原子地更新一个int,你也需要锁住整个临界区。这可能导致不必要的阻塞,因为其他线程可能在等待一个与它们无关的变量的锁释放。
  3. 死锁风险: 互斥锁如果使用不当,很容易引入死锁。例如,如果两个线程各自持有对方需要的锁,就会陷入僵局。原子操作则没有死锁的概念,因为它不涉及资源的“持有”和“等待”。

所以,当你的需求只是对一个简单的变量进行原子性的读、写或读-改-写操作时,std::atomic通常是更高效、更轻量级的选择。它通过直接利用CPU提供的原子指令(如LOCK XADDCMPXCHG等)来实现,避免了操作系统层面的开销,性能上通常优于互斥锁。当然,如果你的操作涉及多个变量或复杂的逻辑,那么互斥锁依然是不可替代的。选择哪种,关键在于理解它们的底层机制和各自的适用场景。

std::atomic 支持哪些类型,以及其内存序(Memory Order)如何影响程序行为?

std::atomic 并非支持所有类型,但它覆盖了绝大多数我们日常会用到的基本数据类型和指针类型。具体来说,它可以包装:

  • 所有基本整数类型: bool, char, short, int, long, long long 及其无符号版本。
  • 浮点类型: float, double, long double(虽然标准支持,但实际中原子操作在浮点数上可能需要软件模拟,性能不一定高)。
  • 指针类型: T*,任何对象的指针。
  • 自定义类型: 如果自定义类型满足以下条件,也可以被std::atomic包装:
    • 没有用户定义的拷贝赋值运算符。
    • 没有用户定义的移动赋值运算符。
    • 没有用户定义的析构函数。
    • 是可平凡复制的(Trivially Copyable)。
    • 所有非静态数据成员都是可平凡复制的。
    • 通常,这意味着你的自定义类型应该是一个简单的结构体,只包含基本类型或指针,并且不涉及复杂的资源管理。

对于自定义类型,你可以通过std::atomic<MyStruct> my_atomic_struct;来使用。不过,std::atomic保证的是对MyStruct实例的整体读写是原子的,而不是其内部成员的原子性。如果MyStruct内部有多个成员需要独立原子访问,那可能需要更复杂的同步机制。

此外,std::atomic_flag 是一个非常特殊的原子类型,它只支持两种操作:test_and_set()clear(),通常用于实现自旋锁,是所有原子类型中最简单、开销最小的。

内存序(Memory Order)如何影响程序行为?

这部分内容说实话,刚接触的时候真的有点让人头疼,因为它直接触及了编译器优化和CPU乱序执行的底层原理。简单来说,内存序就是用来告诉编译器和CPU,在多线程环境下,你的内存操作(读、写)应该以什么样的顺序被其他线程看到。它决定了不同线程之间数据可见性的保证强度。

C++11引入了六种内存序:

  1. std::memory_order_relaxed (松散序)

    • 这是最弱的内存序。它只保证操作本身的原子性,不提供任何跨线程的内存同步或顺序保证。
    • 编译器和CPU可以随意重排relaxed操作,只要不改变当前线程的执行结果。
    • 适用场景: 当你只需要一个原子计数器,而不在乎计数器更新的顺序,也不需要这个计数器与其他内存操作建立任何顺序关系时。例如,统计某个事件发生的次数。
  2. std::memory_order_release (释放序)

    • std::memory_order_acquire配对使用。
    • 写操作使用release语义。它保证所有在release操作之前的内存写入操作,都会在release操作完成之后对其他线程可见。
    • 可以理解为,release操作像一个“栅栏”,它之前的内存操作不能被重排到它之后。
  3. std::memory_order_acquire (获取序)

    • std::memory_order_release配对使用。
    • 读操作使用acquire语义。它保证所有在acquire操作之后的内存读取操作,都会在acquire操作完成之后执行。同时,它能看到所有在配对的release操作之前的内存写入。
    • acquire操作像另一个“栅栏”,它之后的内存操作不能被重排到它之前。

    acquirerelease的配合使用,可以在两个线程间建立“happens-before”关系。一个线程的release操作happens-before另一个线程的acquire操作,那么release之前的所有内存写入,都会在acquire之后对获取线程可见。这是实现无锁数据结构的关键。

    std::atomic<bool> ready_flag(false);
    int data = 0;
    
    void writer_thread() {
        data = 42; // (1) 写入数据
        ready_flag.store(true, std::memory_order_release); // (2) 释放操作
    }
    
    void reader_thread() {
        while (!ready_flag.load(std::memory_order_acquire)) { // (3) 获取操作
            std::this_thread::yield();
        }
        std::cout << "Data: " << data << std::endl; // (4) 读取数据
    }
    // 在这个例子中,由于release/acquire语义,(1) happens-before (2),(2) happens-before (3),(3) happens-before (4)。
    // 所以,当reader线程看到ready_flag为true时,它保证能看到data = 42。
    登录后复制
  4. std::memory_order_acq_rel (获取-释放序)

    • 用于读-改-写操作(如fetch_addcompare_exchange)。
    • 它同时具有acquirerelease的语义:它能看到所有在它之前的release操作的写入,并且它之前的写入操作都会在它完成之后对其他线程可见。
  5. std::memory_order_seq_cst (顺序一致性)

    • 这是最强、也是默认的内存序。
    • 它不仅提供了acquirerelease的所有保证,还额外保证了所有seq_cst操作在所有线程中都以相同的全序执行。
    • 优点: 易于理解和使用,因为它提供了最直观的内存模型,几乎不可能出现意外的重排。
    • 缺点: 性能开销最大,因为它可能需要额外的内存屏障指令来强制全局排序,即使在某些场景下这种严格的排序是不必要的。

在实际开发中,如果对内存序没有深入理解,最安全的做法是使用默认的std::memory_order_seq_cst。只有在确认性能是瓶颈且对并发模型有充分理解时,才考虑使用更弱的内存序。过度优化内存序,往往会引入难以调试的并发bug。

如何在实际项目中选择合适的原子操作和避免常见陷阱?

在真实项目中,选择合适的原子操作并避免陷阱,是保证多线程程序正确性和性能的关键。这需要对std::atomic有比较深入的理解和实践经验。

选择合适的原子操作:

  1. 简单读写:load()store()

    • 当你只需要原子地读取或写入一个变量时,直接使用atomic_var.load()atomic_var.store(value)
    • 这是最基本也是最常用的原子操作,性能通常很高。
    • 内存序的选择:如果只是一个简单的标志位,不与其他内存操作建立顺序关系,std::memory_order_relaxed可能足够。但如果这个标志位的变化需要保证其他数据可见性,那么acquire/releaseseq_cst是必要的。
  2. 读-改-写(RMW)操作:fetch_add(), fetch_sub(), exchange()

    • 当你需要在一个原子操作中读取变量的值,然后基于这个值进行修改,再写回变量时,RMW操作是你的朋友。
    • 例如,fetch_add(1)会原子地将变量加1,并返回加1前的值。
    • 这些操作通常比手动load()modifystore()再加锁要高效得多,因为它们在硬件层面就能保证原子性。
    • exchange(new_value):原子地将变量设置为new_value,并返回旧值。
  3. 比较并交换(CAS):compare_exchange_weak()compare_exchange_strong()

    • 这是实现无锁数据结构(如无锁队列、)的核心。
    • compare_exchange_strong(expected, desired):如果当前原子变量的值等于expected,则将其原子地设置为desired,并返回true;否则,不改变原子变量的值,并将当前值写入expected,返回false
    • compare_exchange_weak():与strong类似,但它可能在值相等时“虚假失败”(spurious failure),即返回false但实际上值是相等的。这通常发生在某些硬件架构上,为了性能考虑,它不会重试。它通常用在循环中,例如do { ... } while (!atomic_var.compare_exchange_weak(...));
    • 选择: 如果你在一个循环中反复尝试CAS,weak可能性能更好,因为它避免了不必要的重试开销。如果CAS操作只执行一次,或者你不能容忍虚假失败,strong是更安全的选择。

避免常见陷阱:

  1. ABA问题:

    • 描述: 假设线程A读取变量X的值为A,然后被调度出去。线程B修改X为B,然后又改回A。线程A恢复执行,发现X的值仍然是A,认为没有被修改过,然后执行CAS操作成功。但实际上,X在中间被修改过。这在一些无锁数据结构中可能导致逻辑错误,例如,一个节点被弹出后又被重新插入到链表中,导致线程A操作了一个“陈旧”的指针。
    • 规避:
      • 使用版本号或标记:将原子变量包装成一个结构体,包含实际值和一个版本号(或计数器)。每次修改值时,同时递增版本号。CAS操作时,同时比较值和版本号。
      • C++标准库中的std::atomic<std::shared_ptr<T>>可以自动处理ABA问题,因为它内部通常会维护一个版本计数器。
  2. 伪共享(False Sharing):

    • 描述: 多个线程访问不同的原子变量,但这些原子变量恰好位于同一个CPU缓存行(cache line)中。当一个线程修改其原子变量时,整个缓存行会被标记为脏,并需要同步到其他CPU核心,导致其他核心的缓存失效。即使这些线程访问的是完全不同的数据,但由于它们共享同一个缓存行,也会引发不必要的缓存同步开销,严重影响性能。
    • 规避:
      • 缓存行对齐: 使用alignas(std::hardware_destructive_interference_size)(C++17)或手动填充(padding)来确保不同的原子变量位于不同的缓存行。
      • 将经常被不同线程访问的原子变量分隔开。
  3. 内存序的误用:

    • 描述: 错误地使用std::memory_order_relaxedstd::memory_order_acquire/release,导致程序在某些CPU架构或编译器优化下出现数据可见性问题,产生难以复现的bug。例如,忘记在release操作前写入数据,或在acquire操作后读取数据。
    • 规避:
      • 默认使用std::memory_order_seq_cst 这是最安全的选项,除非你确定需要优化性能并且对内存模型有深入理解。
      • 理解acquire/release语义: 牢记release操作前的所有写入对配对的acquire操作后的所有读取可见。如果你的数据流需要这种顺序保证,就必须使用它们。
      • 谨慎使用relaxed 只有当你明确知道操作的顺序对其他线程不重要时才使用。
  4. 混合原子与非原子访问:

    • 描述: 对同一个变量,有时使用std::atomic进行操作,有时又直接进行非原子操作。这会导致数据竞争,因为非原子操作不会受到任何同步保证。
    • 规避:
      • 一旦一个变量被声明为std::atomic,就应该始终通过其原子接口进行访问。
  5. 并非所有原子操作都是无锁的:

    • 描述: std::atomic不保证其所有操作都是“无锁”的(即不使用操作系统互斥锁)。对于某些复杂类型或某些平台,std::atomic可能在内部使用互斥锁来模拟原子性。
    • 规避:
      • 使用atomic_var.is_lock_free()来检查一个特定的std::atomic实例是否是真正无锁的。如果返回false,那么它的性能可能不如预期,甚至可能比std::mutex更差。

总的来说,原子操作是C++并发编程的强大工具,但它并非银弹。理解其底层原理、内存序以及潜在的陷阱,并在实际项目中谨慎选择和使用,才能真正发挥其优势。

以上就是c++++如何使用原子操作atomic_c++多线程原子操作库应用的详细内容,更多请关注php中文网其它相关文章!

c++速学教程(入门到精通)
c++速学教程(入门到精通)

c++怎么学习?c++怎么入门?c++在哪学?c++怎么学才快?不用担心,这里为大家提供了c++速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号