C++std::atomic可以使用复杂类型(类和结构体)吗
- IT业界
- 2025-09-09 14:06:01

目录
1.引言
2.std::atomic 支持的复杂类型
3.std::atomic与无锁
4.如何使用 std::atomic 保护复杂类型
4.1.使用互斥锁(Mutex)
4.2.使用 std::atomic_flag 和自旋锁
4.4.使用高级同步机制
5.std::atomic 和 volatile 的区别
6.总结
1.引言
std::atomic 是 C++11 标准库中的一个模板类,用于提供原子操作。原子操作是不可分割的操作,即在多线程环境下执行时,不会被线程调度机制打断。这保证了在多线程编程中,对共享数据的访问是安全的,避免了数据竞争和不一致性问题。
std::atomic 可以用于基本数据类型,如整型(int, long, bool 等)和指针类型。它提供了一套成员函数,用于执行原子读、写、交换、比较并交换等操作。如:
#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 就能得到答案:“不!”,因为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 导致竞态条件、对内存顺序的误解、复合操作的不原子性。
参考:
en.cppreference /w/cpp/atomic/atomic
C++std::atomic可以使用复杂类型(类和结构体)吗由讯客互联IT业界栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“C++std::atomic可以使用复杂类型(类和结构体)吗”