C++ std::atomic的用法(非常详细)
在 C++ 中,std::atomic 是为了实现线程间的原子操作而设计的。
原子操作是那些在多线程环境中不会被线程调度机制中断的操作,也就是说,这些操作一旦开始就会一直运行到结束,中间不会被其他线程打断。
std::atomic 提供了一种方式,可以在多线程程序中安全地操作数据,而无须使用互斥锁。这些特性在实现锁自由数据结构和算法中非常有用。
例如:
对于一个类型 T,要使用 std::atomic<T>,T 必须是平凡可复制的。这意味着类型 T 必须同时具备平凡的拷贝构造函数、平凡的赋值运算符以及平凡的析构函数,因为原子类型的操作依赖于类型能够通过简单的内存复制来进行值的传递。
此外,为了确保原子操作可以安全且一致地执行,类型T的大小必须是固定的且在编译时已知。这样,std::atomic 才能保证在进行原子操作时,可以作为一个完整的内存单元来进行处理,确保操作的原子性。
另外,对于某些特定平台,如果一个类型 T 是平凡可复制的且大小合适,那么它可以使用 std::atomic,尽管这并不保证跨所有平台的兼容性。例如,某些编译器支持使用 std::atomic 对特定的自定义类型进行操作,但这依赖于编译器和平台的内存模型。
当设计自定义类型时,要考虑该类型是否适合作为 std::atomic 的模板参数。下面是一个简单的示例,展示一个自定义类型如何适用于原子操作。
然后,我们希望能够在多线程环境中安全地对 Point 对象进行原子操作。因此,尝试将 Point 类型用作 std::atomic<Point> 的模板参数。然而,为了能够安全地使用 std::atomic,必须保证 Point 类型是平凡的拷贝构造可析构的类型。这意味着它的拷贝构造函数和析构函数必须是默认的,并且不执行任何操作。因此,我们添加了静态断言来确保它满足了所需的条件。如果 Point 类型不满足这些条件,编译器将在编译时产生错误。
最后,尝试在 main() 函数中创建一个 std::atomic<Point> 对象,并对它进行一些基本的原子操作,以验证我们的自定义类型是否可以安全地用作 std::atomic 的模板参数。
然而,随着并发编程的需求日益增长和硬件能力的逐步提升,C++20 引入了对浮点数类型(如 float、double 和 long double)进行原子操作的支持,例如 fetch_add 和 fetch_sub。这增强了在并发环境中进行科学计算和数据处理的能力。
C++23 进一步扩展了对浮点数的支持,包括对扩展的浮点类型(如 cv-unqualified extended floating-point types)进行原子操作的支持,进一步增强了并发编程在处理复杂数值类型时的能力。这些扩展确保了 C++ 在并发和高性能计算领域的持续竞争力和适应性。
原子操作是那些在多线程环境中不会被线程调度机制中断的操作,也就是说,这些操作一旦开始就会一直运行到结束,中间不会被其他线程打断。
std::atomic 提供了一种方式,可以在多线程程序中安全地操作数据,而无须使用互斥锁。这些特性在实现锁自由数据结构和算法中非常有用。
例如:
#include <atomic> #include <iostream> #include <thread> std::atomic<int> count(0); void increment() { for (int i = 0; i < 10000; ++i) { count.fetch_add(1, std::memory_order_relaxed); } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Count: " << count << '\n'; return 0; }在这个例子中,fetch_add 是一个原子操作,它安全地增加一个 std::atomic<int> 类型的变量。
C++ std::atomic适用条件
std::atomic 在 C++ 中并不适用于任何数据类型,主要适用于整数类型和指针类型。对于更复杂的数据类型,如自定义类或结构体,std::atomic 不直接支持,除非这些类型满足特定的条件。对于一个类型 T,要使用 std::atomic<T>,T 必须是平凡可复制的。这意味着类型 T 必须同时具备平凡的拷贝构造函数、平凡的赋值运算符以及平凡的析构函数,因为原子类型的操作依赖于类型能够通过简单的内存复制来进行值的传递。
此外,为了确保原子操作可以安全且一致地执行,类型T的大小必须是固定的且在编译时已知。这样,std::atomic 才能保证在进行原子操作时,可以作为一个完整的内存单元来进行处理,确保操作的原子性。
另外,对于某些特定平台,如果一个类型 T 是平凡可复制的且大小合适,那么它可以使用 std::atomic,尽管这并不保证跨所有平台的兼容性。例如,某些编译器支持使用 std::atomic 对特定的自定义类型进行操作,但这依赖于编译器和平台的内存模型。
当设计自定义类型时,要考虑该类型是否适合作为 std::atomic 的模板参数。下面是一个简单的示例,展示一个自定义类型如何适用于原子操作。
#include <atomic> #include <iostream> #include <type_traits> // 自定义类型 Point struct Point { int x; int y; // 默认构造函数 Point() : x(0), y(0) {} // 自定义构造函数 Point(int x, int y) : x(x), y(y) {} // 拷贝构造函数和拷贝赋值运算符 Point(const Point&) = default; Point& operator=(const Point&) = default; // 析构函数 ~Point() = default; }; int main() { static_assert(std::is_trivially_copyable<Point>::value, "Point must be trivially copyable"); std::atomic<Point> atomic_point; Point p1(1, 2); atomic_point.store(p1); Point p2 = atomic_point.load(); std::cout << "Atomic Point: (" << p2.x << ", " << p2.y << ")" << std::endl; return 0; }运行结果为:
Atomic Point: (1, 2)
在这个示例中,首先定义了一个自定义类型 Point,代表一个二维平面上的点。Point 类型包含两个整数成员变量 x 和 y,用来表示点的坐标。然后,我们希望能够在多线程环境中安全地对 Point 对象进行原子操作。因此,尝试将 Point 类型用作 std::atomic<Point> 的模板参数。然而,为了能够安全地使用 std::atomic,必须保证 Point 类型是平凡的拷贝构造可析构的类型。这意味着它的拷贝构造函数和析构函数必须是默认的,并且不执行任何操作。因此,我们添加了静态断言来确保它满足了所需的条件。如果 Point 类型不满足这些条件,编译器将在编译时产生错误。
最后,尝试在 main() 函数中创建一个 std::atomic<Point> 对象,并对它进行一些基本的原子操作,以验证我们的自定义类型是否可以安全地用作 std::atomic 的模板参数。
C++浮点数类型的原子操作
需要注意的是,在 C++20 之前,虽然已广泛支持整数的原子操作,但对于浮点数类型,则尚未直接支持原子操作。这主要是由于早期的标准和多数硬件对浮点数的原子操作支持不足,以及标准库的逐步发展策略。然而,随着并发编程的需求日益增长和硬件能力的逐步提升,C++20 引入了对浮点数类型(如 float、double 和 long double)进行原子操作的支持,例如 fetch_add 和 fetch_sub。这增强了在并发环境中进行科学计算和数据处理的能力。
C++23 进一步扩展了对浮点数的支持,包括对扩展的浮点类型(如 cv-unqualified extended floating-point types)进行原子操作的支持,进一步增强了并发编程在处理复杂数值类型时的能力。这些扩展确保了 C++ 在并发和高性能计算领域的持续竞争力和适应性。