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

如何在C++中使用std::atomic进行原子操作_C++原子操作与无锁编程

尼克
发布: 2025-09-22 18:18:01
原创
771人浏览过
原子操作通过互斥访问共享数据实现线程安全,C++中std::atomic提供原子读写能力。其核心操作包括load、store、exchange及compare_exchange_weak/strong,后者常用于无锁算法。示例中多个线程对std::atomic<int> counter进行递增,确保结果正确为40000。内存顺序如memory_order_relaxed至memory_order_seq_cst影响同步强度与性能,需根据需求选择以平衡效率与一致性。自旋锁可用std::atomic<bool>实现,通过exchange和store配合acquire-release语义完成。使用陷阱包括伪共享、ABA问题、内存泄漏和死锁,需采用填充、版本号、Hazard Pointer等技术规避。

如何在c++中使用std::atomic进行原子操作_c++原子操作与无锁编程

原子操作的核心在于保证多线程环境下对共享数据的访问是互斥的,避免数据竞争,从而实现线程安全。C++的

std::atomic
登录后复制
模板类提供了这种能力,允许你以原子方式读取、写入和修改变量,而无需显式地使用锁。

解决方案:

使用

std::atomic
登录后复制
的关键在于理解其提供的操作。最常用的包括:

  • load()
    登录后复制
    : 原子地读取值。
  • store()
    登录后复制
    : 原子地存储值。
  • exchange()
    登录后复制
    : 原子地用新值替换旧值,并返回旧值。
  • compare_exchange_weak()
    登录后复制
    compare_exchange_strong()
    登录后复制
    : 原子地比较当前值与预期值,如果相等则用新值替换,否则不替换。这两个函数是实现无锁算法的基础。
    compare_exchange_weak()
    登录后复制
    在某些平台上可能由于伪失败(spurious failure)而返回失败,即使当前值与预期值相等,因此通常在一个循环中使用。
    compare_exchange_strong()
    登录后复制
    则保证只有在当前值与预期值不相等时才会返回失败。

下面是一个简单的例子,展示了如何使用

std::atomic
登录后复制
来递增一个共享计数器:

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

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

std::atomic<int> counter(0); // 初始化原子计数器

void increment_counter() {
  for (int i = 0; i < 10000; ++i) {
    counter++; // 原子递增操作
  }
}

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

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

  std::cout << "Counter value: " << counter << std::endl; // 预期输出:40000
  return 0;
}
登录后复制

在这个例子中,

counter
登录后复制
是一个
std::atomic<int>
登录后复制
类型的原子变量。
counter++
登录后复制
操作会被原子地执行,这意味着即使多个线程同时执行这个操作,
counter
登录后复制
的值也会正确地递增,而不会发生数据竞争。

无锁编程往往比基于锁的编程更加复杂,需要仔细考虑内存模型、数据一致性等问题。

std::atomic
登录后复制
提供了一组工具,可以帮助你构建高效且线程安全的无锁数据结构和算法。

std::atomic
登录后复制
的内存顺序选项是什么?它们如何影响性能?

std::atomic
登录后复制
提供了多种内存顺序选项,用于控制原子操作的同步行为。这些选项包括:

  • std::memory_order_relaxed
    登录后复制
    : 最宽松的内存顺序,只保证操作的原子性,不保证任何同步或排序。
  • std::memory_order_consume
    登录后复制
    : 保证当前线程能够看到依赖于当前原子变量的其它原子变量的最新值。
  • std::memory_order_acquire
    登录后复制
    : 保证当前线程能够看到其它线程在释放(release)同一个原子变量之前的所有写入操作。
  • std::memory_order_release
    登录后复制
    : 保证当前线程的所有写入操作对其它线程在获取(acquire)同一个原子变量之后可见。
  • std::memory_order_acq_rel
    登录后复制
    : 同时具有 acquire 和 release 的语义,通常用于 read-modify-write 操作,例如
    fetch_add
    登录后复制
  • std::memory_order_seq_cst
    登录后复制
    : 默认的内存顺序,提供最强的同步保证,保证所有原子操作都按照一个全局的顺序执行。

选择合适的内存顺序对于性能至关重要。

std::memory_order_relaxed
登录后复制
通常是最快的,因为它不需要任何同步,但只有在不需要同步的情况下才能使用。
std::memory_order_seq_cst
登录后复制
提供最强的同步保证,但也是最慢的。通常,应该尽可能使用较弱的内存顺序,只有在需要更强的同步保证时才使用更强的内存顺序。

举个例子,假设有两个线程 A 和 B,共享一个原子变量

x
登录后复制
。线程 A 执行以下操作:

x.store(1, std::memory_order_release);
登录后复制

线程 B 执行以下操作:

int value = x.load(std::memory_order_acquire);
登录后复制

在这个例子中,

std::memory_order_release
登录后复制
保证线程 A 在存储
x
登录后复制
之前的所有写入操作对线程 B 可见。
std::memory_order_acquire
登录后复制
保证线程 B 在读取
x
登录后复制
之后能够看到线程 A 在存储
x
登录后复制
之前的所有写入操作。

如何使用

std::atomic
登录后复制
实现一个简单的自旋锁?

自旋锁是一种忙等待的锁,线程会不断地检查锁是否可用,直到锁被释放。使用

std::atomic
登录后复制
可以很容易地实现一个自旋锁:

#include <atomic>

class SpinLock {
public:
  SpinLock() : locked(false) {}

  void lock() {
    while (locked.exchange(true, std::memory_order_acquire));
  }

  void unlock() {
    locked.store(false, std::memory_order_release);
  }

private:
  std::atomic<bool> locked;
};
登录后复制

在这个例子中,

locked
登录后复制
是一个
std::atomic<bool>
登录后复制
类型的原子变量,用于表示锁的状态。
lock()
登录后复制
函数使用
exchange()
登录后复制
操作原子地尝试获取锁。如果
locked
登录后复制
的值已经是
true
登录后复制
,则
exchange()
登录后复制
操作会返回
true
登录后复制
,线程会继续循环等待。如果
locked
登录后复制
的值是
false
登录后复制
,则
exchange()
登录后复制
操作会将
locked
登录后复制
的值设置为
true
登录后复制
,并返回
false
登录后复制
,线程成功获取锁。
unlock()
登录后复制
函数使用
store()
登录后复制
操作原子地释放锁。

需要注意的是,自旋锁只适用于锁的持有时间很短的情况。如果锁的持有时间很长,线程会浪费大量的 CPU 时间在忙等待上。在锁的持有时间很长的情况下,应该使用互斥锁(

std::mutex
登录后复制
)或条件变量(
std::condition_variable
登录后复制
)。

使用

std::atomic
登录后复制
时可能遇到的陷阱有哪些?

使用

std::atomic
登录后复制
时需要注意以下几个陷阱:

  • 伪共享(False Sharing): 如果多个线程访问相邻的原子变量,即使这些变量之间没有逻辑关系,也可能导致性能下降。这是因为 CPU 缓存行是以行为单位进行缓存的,如果多个线程访问同一个缓存行中的不同变量,会导致缓存行的频繁失效和重新加载。为了避免伪共享,可以将原子变量分散到不同的缓存行中,例如使用填充(padding)。
  • ABA 问题: 在无锁算法中,如果一个原子变量的值从 A 变为 B,然后再变回 A,可能会导致一些问题。例如,
    compare_exchange_weak()
    登录后复制
    可能会错误地认为原子变量的值没有改变,从而导致算法出错。为了解决 ABA 问题,可以使用版本号或指针标记。
  • 内存泄漏: 在无锁数据结构中,如果一个线程在释放一个节点之前崩溃,可能会导致内存泄漏。为了避免内存泄漏,可以使用 Hazard Pointer 或 RCU(Read-Copy-Update)等技术。
  • 死锁: 虽然
    std::atomic
    登录后复制
    本身可以避免数据竞争,但如果使用不当,仍然可能导致死锁。例如,如果两个线程互相等待对方释放锁,就会导致死锁。

总之,使用

std::atomic
登录后复制
需要仔细考虑各种因素,才能编写出高效且线程安全的无锁代码。

以上就是如何在C++中使用std::atomic进行原子操作_C++原子操作与无锁编程的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

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

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

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