当前位置: 首页 > news >正文

C++ std::atomic可以使用复杂类型(类和结构体)吗

目录

1.引言

2.std::atomic 支持的复杂类型

3.std::atomic与无锁

4.如何使用 std::atomic 保护复杂类型

4.1.使用互斥锁(Mutex)

4.2.使用 std::atomic_flag 和自旋锁

4.3.原子共享指针(Atomic Shared Pointers)

4.4.使用高级同步机制

5.std::atomic 和 volatile 的区别

6.总结


1.引言

        std::atomic 是 C++11 标准库中的一个模板类,用于提供原子操作。原子操作是不可分割的操作,即在多线程环境下执行时,不会被线程调度机制打断。这保证了在多线程编程中,对共享数据的访问是安全的,避免了数据竞争和不一致性问题。

        std::atomic 可以用于基本数据类型,如整型(intlongbool 等)和指针类型。它提供了一套成员函数,用于执行原子读、写、交换、比较并交换等操作。如:

 

#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;
    return 0;
}

        std::atomic 还可以用于更复杂的类型,如自定义类和结构体,但使用复杂类型时有一些额外的限制和注意事项。

2.std::atomic 支持的复杂类型

C++11 标准引入了对复杂类型的原子支持,但需要满足以下条件:

必须是“平凡可复制”的类型

  • 只有**平凡可复制(Trivially Copyable)**的类型才能直接用于 std::atomic

  • 平凡可复制的含义:

    • 没有自定义的构造函数、析构函数、拷贝构造和拷贝赋值运算符。

    • 可以通过 memcpy 直接拷贝的类型。

C++之std::is_trivially_copyable(平凡可复制类型检测)_trivially copyable-CSDN博客

        示例如下:

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

struct Point {
    int x;
    int y;
};

std::atomic<Point> point = Point{0, 0};

void modify_point() {
    Point p = point.load();
    p.x += 1;
    p.y += 1;
    point.store(p);
}

int main() {
    std::thread t1(modify_point);
    std::thread t2(modify_point);
    t1.join();
    t2.join();
    Point p = point.load();
    std::cout << "Point: (" << p.x << ", " << p.y << ")\n";
    return 0;
}

输出:

Point: (2, 2) 

解释: 

  • Point 是一个平凡可复制的类型,因此可以直接用 std::atomic<Point>

  • 由于原子性操作是对整个结构体进行的,但这里存在竞态条件,fetch_add 不适用于结构体。

不支持的类型示例(非平凡可复制):

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

struct Person {
    std::string name;
    int age;
};

std::atomic<Person> person = Person{"Alice", 30}; // ❌ 编译错误

int main() {
    return 0;
}

报错原因:

  • std::string 是一个复杂类型,包含动态内存分配,因此不是平凡可复制的类型。

3.std::atomic与无锁

   std::atomic<T>一定是无锁的吗?其实只要你花一点时间去翻一下cppreference.com就能得到答案:“不!”,因为std::atomic<T>提供了一个方法叫is_lock_free

        考虑以下几个结构体:

struct A { long x; };
struct B { long x; long y; };
struct C { char s[1024]; };

        A应当是无锁的,因为它显然等价于long。C应该不是无锁的,因为它实在是太大了,目前没有寄存器能存下它。至于B我们就很难直接推断出来了。

        对于x86架构的CPU,结构体B应当是无锁的,它刚刚好可以原子地使用MMX寄存器(64bit)处理。但如果它再大一点(比如塞一个int进去),它就不能无锁地处理了。

        原子操作究竟是否无锁与CPU的关系很大。如果CPU提供了丰富的用于无锁处理的指令与寄存器,则能够无锁执行的操作就会多一些,反之则少一些。除此之外,原子操作能否无锁地执行还与内存对齐有关。正因如此,is_lock_free()才会是一个运行时方法,而没有被标记为constexpr

4.如何使用 std::atomic 保护复杂类型

4.1.使用互斥锁(Mutex)

使用互斥锁(如 std::mutex)来保护对复杂类型数据的访问。这是最常见的方法,可以确保在任何时候只有一个线程可以访问数据。

#include <iostream>
#include <thread>
#include <mutex>

struct ComplexType {
    int a;
    double b;
    std::string c;
};

ComplexType data;
std::mutex dataMutex;

void modifyData(int newA, double newB, const std::string& newC) {
    std::lock_guard<std::mutex> lock(dataMutex);
    data.a = newA;
    data.b = newB;
    data.c = newC;
}

void printData() {
    std::lock_guard<std::mutex> lock(dataMutex);
    std::cout << "a: " << data.a << ", b: " << data.b << ", c: " << data.c << std::endl;
}

int main() {
    std::thread t1(modifyData, 10, 20.5, "Hello");
    std::thread t2(printData);

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

    return 0;
}

4.2.使用 std::atomic_flag 和自旋锁

对于复杂类型,可以使用原子标志位实现自旋锁,保护整个临界区。

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

struct Person {
    std::string name;
    int age;
};

Person person = {"Alice", 30};
std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;

void lock() {
    while (lock_flag.test_and_set(std::memory_order_acquire)) {} // 自旋等待
}

void unlock() {
    lock_flag.clear(std::memory_order_release);
}

void update_person() {
    lock();
    person.age += 1;
    std::cout << "Updated Age: " << person.age << "\n";
    unlock();
}

int main() {
    std::thread t1(update_person);
    std::thread t2(update_person);
    t1.join();
    t2.join();
    std::cout << "Final Age: " << person.age << "\n";
    return 0;
}

解释:

  • 使用了 std::atomic_flag 实现了一个简单的自旋锁,保护了对 Person 结构体的访问。

  • 由于 std::string 是非平凡可复制类型,这里需要使用自旋锁来保护。

4.3.原子共享指针(Atomic Shared Pointers)

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

#include <atomic>
#include <memory>
#include <iostream>

struct ComplexData {
    int value1;
    double value2;
    std::string value3;

    ComplexData(int v1, double v2, std::string v3)
        : value1(v1), value2(v2), value3(v3) {}
};

std::atomic<std::shared_ptr<ComplexData>> dataPtr;

void updateData(int v1, double v2, const std::string& v3) {
    // 为新数据创建一个shared_ptr
    std::shared_ptr<ComplexData> newData = std::make_shared<ComplexData>(v1, v2, v3);

    // 原子地替换旧数据
    std::atomic_store(&dataPtr, newData);
}

void processData() {
    // 原子地读取共享指针
    std::shared_ptr<ComplexData> data = std::atomic_load(&dataPtr);
    if (data) {
        std::cout << "Current Data: " << data->value1 << ", " << data->value2 << ", " << data->value3 << std::endl;
    } else {
        std::cout << "Data pointer is null." << std::endl;
    }
}

int main() {
    updateData(10, 20.5, "Test Data");
    processData();

    return 0;
}

在这个代码中:

  • ComplexData 结构体用于存储一个整数、一个双精度浮点数和一个字符串。
  • dataPtr 是一个原子性的 std::shared_ptr<ComplexData>,用于确保对复杂数据的线程安全访问。
  • updateData 函数创建一个新的 ComplexData 实例,并原子性地替换旧数据。
  • processData 函数原子性地读取数据并打印。

这样,即使在多线程环境中,对 dataPtr 的访问也是安全的,避免了数据竞争的问题。

4.4.使用高级同步机制

对于更复杂的同步需求,你可以考虑使用条件变量(std::condition_variable)、信号量(std::binary_semaphore,C++20 引入)等高级同步机制。

5.std::atomic 和 volatile 的区别

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

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

6.总结

        std::atomic 提供轻量、高效的原子操作,适用于不需要复杂锁机制的场景。同时需理解内存顺序对性能和正确性的影响。对于复杂的多操作组合,仍需谨慎处理竞态条件。不当使用 load 和 store 导致竞态条件、对内存顺序的误解、复合操作的不原子性。

参考:

https://en.cppreference.com/w/cpp/atomic/atomic

http://www.dtcms.com/a/20559.html

相关文章:

  • 【C++】vector的使用练习 + 模拟实现
  • pnpm和npm安装TailwindCss
  • 【C++】34.智能指针(1)
  • 2025年免费量化交易软件——PTrade(含开通攻略)
  • JavaScript 中的“无限套娃”与“魔法优化”:递归与尾调用优化(TCO)
  • 2025年前端工程师职业发展的系统性应聘规划
  • 【效率技巧】怎么做思维导图||数学思维||费曼学习法
  • 算法笔记——字典树
  • Leetcode 712. Minimum ASCII Delete Sum for Two Strings
  • 机器学习 - 学习线性模型的重要性
  • 智能交通路线规划:让 AI 帮你躲避拥堵
  • Express 路由路径正则详解
  • Python随笔
  • 大模型炼丹基础--GPU内存计算
  • Redis c++安装使用教程(redis-plus-plus)
  • LabVIEW利用CANopen的Batch SDO写入
  • 乘积最大 之 连续与非联系子数组
  • Linux上Scapy完全指南:从入门到实战攻防
  • 基于SpringBoot实现的大学社团平台系统实现功能六
  • 【16届蓝桥杯寒假刷题营】第2期DAY1I
  • Visionpro 齿轮测量
  • frp与云服务器内网穿透
  • 网上购物|基于SprinBoot+vue的网上购物系统(源码+数据库+文档)
  • 大模型应用开发时如何调试提示词?
  • 深入解析504网关超时错误:服务器通信故障的诊断与解决
  • 每日复盘20250216
  • TensorFlow项目GPU运行 安装步骤
  • ThreadLocal详解
  • 2.buuctf [CISCN 2019 初赛]Love Math
  • Httprint 指纹识别技术:网络安全的关键洞察