在并发环境中,如何保证数据在多个线程之间的同步与一致性是一个关键问题。传统的同步方式使用互斥锁(mutex
)来保护共享数据,但锁的使用带来了额外的开销。为了解决这个问题,C++11 提供了 atomic<T>
模板类,它允许在不使用锁的情况下安全地进行多线程编程。atomic<T>
提供了对基础数据类型的原子操作支持,能够有效避免竞争条件,提升并发性能。
一、什么是 atomic<T>
?
atomic<T>
是 C++11 提供的一个模板类,用于在多线程环境中安全地操作基础数据类型。它通过底层 CPU 提供的指令集,保证了对变量的操作是原子性的,即每个操作要么完整地执行,要么根本不执行,不会因为线程切换导致数据不一致。
支持的类型
atomic<T>
模板类支持以下类型:
- 基础整型(如
int
、long
、unsigned int
) - 布尔类型(
bool
) - 指针类型
值得注意的是,atomic<T>
不支持浮点类型和自定义数据类型。
二、atomic<T>
的常用操作
atomic<T>
提供了一组线程安全的操作,用于读取、修改、比较交换等。其主要操作函数包括:
-
构造函数
atomic() noexcept
:默认构造函数,初始化一个原子变量。atomic(T val) noexcept
:使用初始值val
初始化原子变量。atomic(const atomic&) = delete
:禁用拷贝构造函数,防止原子变量被复制。
-
赋值操作
atomic& operator=(const atomic&) = delete
:禁用赋值操作,防止赋值导致的竞争问题。
-
常用方法
void store(const T val) noexcept
:存储一个值val
到原子变量。T load() noexcept
:读取原子变量的值。T fetch_add(const T val) noexcept
:将原子变量的值与val
相加,并返回原值。T fetch_sub(const T val) noexcept
:将原子变量的值减去val
,并返回原值。T exchange(const T val) noexcept
:将原子变量的值替换为val
,并返回原值。bool compare_exchange_strong(T &expect, const T val) noexcept
:比较原子变量的值与expect
,如果相等则将其替换为val
,并返回true
;否则将原子变量的值更新为expect
,并返回false
。
-
性能查询
bool is_lock_free()
:查询某原子类型的操作是否是无锁的。如果返回true
,表示操作由 CPU 原生指令直接支持;如果返回false
,表示编译器使用了内部的锁机制来保证操作的安全性。
三、原子操作示例
以下是一些 atomic<T>
操作的示例,展示了如何在多线程环境中安全地使用原子变量。
示例 1:基本操作
|
|
示例 2:比较交换(CAS)
CAS(Compare-And-Swap)是实现无锁算法的基础操作。它允许我们在读取原子变量时,检查其当前值是否与预期值匹配,如果匹配则进行更新。
|
|
在这个例子中,compare_exchange_strong()
尝试将 ii
的值从 3
更新为 5
,但由于 expect
是 4
,因此操作失败,ii
保持原值 3
,同时 expect
被更新为 ii
的当前值。
四、使用场景
4.1 计数器
原子整型可以用作计数器,在并发环境下安全地递增或递减。
|
|
4.2 布尔开关
原子布尔型可以用作线程间的信号传递,用来实现轻量级的开关机制。
|
|
4.3 无锁队列
原子操作是实现无锁数据结构的基础,CAS 指令可以保证数据结构的线程安全性,而无需使用互斥锁。无锁队列广泛应用于高性能、多线程场景中。
总结
atomic<T>
是 C++11 中为多线程编程提供的重要工具。它利用底层硬件的支持,实现了对基础数据类型的无锁访问和修改操作。在适当的场景下使用原子操作,能够避免锁带来的性能开销,并且能够简化代码逻辑。然而,虽然原子操作可以避免锁的使用,但它们也有自己的局限性。