1 原子操作的基本概念
1.1 什么是原子操作
原子操作(Atomic Operations)是一种在多线程环境下对数据的访问和修改方式,这种操作是不可分割的,即在执行过程中不会被其他线程中断或干扰。原子操作确保了在多线程环境中对共享数据的访问和修改的一致性和完整性。
原子操作的核心概念是"原子性",它源自于数据库管理系统中的 ACID 属性,其中 "A" 代表原子性。在多线程编程中,原子性确保了一组操作要么完全执行,要么完全不执行,不会出现只执行了部分操作的情况。
在 C++11 之前,程序员通常使用互斥锁(Mutex)或其他同步机制来确保多线程环境下的数据一致性。然而,互斥锁的使用可能会引入额外的开销,降低程序的性能。C++11 引入原子操作库 <atomic>,提供了一组原子数据类型和操作,使程序员能够更高效地处理多线程环境下的数据共享问题。
C++11 中的原子操作类型包括 std::atomic<T> 以及针对特定类型(如 bool、int、char 等)的特化版本。这些原子类型保证了在多个线程中对共享数据的访问和修改是原子性的。编译器会确保针对这些原子类型的操作不会被其他线程打断或干扰。
原子操作库提供了一系列函数来执行原子操作,如加载(load)、存储(store)、交换(exchange)、比较并交换(compare_exchange)以及原子算术和位操作等。这些操作都是线程安全的,可以在多线程环境中安全地使用。
原子操作的优点在于它们避免了使用互斥锁所带来的开销,提高了程序的效率。同时,原子操作也简化了多线程编程的复杂性,使程序员能够更容易地处理共享数据的并发访问问题。
需要注意的是,虽然原子操作能够解决一些多线程并发访问的问题,但它们并非万能的。在某些复杂的多线程场景中,可能仍需要结合其他同步机制(如互斥锁、条件变量等)来实现更高级别的并发控制。
1.2 C++11 的原子操作
C++11 中的原子操作是一种特殊的操作,它确保了当一个线程在执行某个操作时,不会被其他线程打断,从而保证了对共享数据的线程安全访问。原子操作在多线程编程中起着至关重要的作用,它使得多个线程能够协同工作,而不会相互干扰或产生数据竞争。
在 C++11 中,原子操作是通过 std::atomic 模板类实现的,该模板类可以包装任何数据类型,使其具备原子性。
C++11 的原子操作具有以下几个关键特点:
- 不可分割性:原子操作是不可分割的,即在执行过程中不会被其他线程打断。
- 线程安全:由于原子操作的不可分割性,多个线程对同一原子变量的访问和修改不会引发数据竞争,从而保证了线程安全。
- 无锁操作:原子操作通常不需要使用锁(如互斥锁)来实现同步,因此具有更高的性能。
应用场景:
原子操作在 C++11 多线程编程中广泛应用,特别是在需要共享数据访问和修改的场景下。以下是一些常见的应用场景:
- 计数器:在多线程环境中,经常需要维护一个全局计数器。通过使用原子操作,可以确保多个线程对计数器的递增或递减操作是线程安全的。
- 状态标志:在某些场景下,需要使用原子操作来修改一个状态标志。例如,一个线程可能需要检查并修改一个共享标志位,以表示某个任务已经完成或某个条件已经满足。
- 数据结构的线程安全访问:对于某些复杂的数据结构(如链表、树等),可能需要在多线程环境中进行插入、删除或查找操作。通过使用原子操作,可以确保对这些数据结构的访问和修改是线程安全的。
- 无锁数据结构:原子操作是实现无锁数据结构的关键。无锁数据结构通过原子操作来避免使用锁,从而提高了性能。虽然无锁数据结构的设计和实现相对复杂,但在某些高性能应用场景下非常有用。
2 原子类型
2.1 std::atomic
std::atomic<T> 是 C++11 中引入的一个模板类,用于提供原子类型的操作。原子操作是指一系列不可分割的指令,即这些指令要么全部执行,要么全部不执行,不存在执行到一半被其他线程打断的情况。在多线程编程中,原子操作是非常重要的,因为它们能确保共享数据的一致性,防止数据竞争。
std::atomic<T> 允许开发者定义任意类型的原子变量,其中 T 可以是任意类型,例如 bool、char、int、long、指针等(但需要注意的是,C++11 的 std::atomic 并不支持浮点类型和复合类型)。
std::atomic<T>的主要特点包括:
- 线程安全:多个线程可以同时访问和修改std::atomic<T>类型的变量,而不需要额外的同步机制(如互斥锁),因为原子操作本身就是线程安全的。
- 无锁操作:std::atomic<T>提供了无锁的数据访问和修改方式,这通常比使用锁的方式具有更高的性能,因为无锁操作可以避免锁带来的开销和潜在的死锁问题。
- 内存序:std::atomic<T>的操作还支持多种内存序(memory order),允许开发者根据具体的并发需求来调整操作的顺序和可见性。
std::atomic<T>提供了一些成员函数来操作原子变量,例如:
- load():原子地加载并返回原子变量的当前值。
- store(T desired):原子地将desired值存储到原子变量中。
- exchange(T desired):原子地将原子变量的当前值与desired进行交换,并返回旧值。
- compare_exchange_weak(T& expected, T desired) 和 compare_exchange_strong(T& expected, T desired):这两个函数尝试将原子变量的值与 desired 进行比较,如果相等,则将原子变量的值设置为 desired,并返回 true;如果不相等,则将expected设置为原子变量的当前值,并返回 false。这两个函数的区别在于它们在处理并发冲突时的行为略有不同。
此外,std::atomic<T> 还支持一些复合操作,如 fetch_add、fetch_sub 等,这些操作在修改原子变量的同时返回旧值,这对于实现一些特定的并发算法和模式非常有用。
需要注意的是,虽然 std::atomic<T> 提供了线程安全的原子操作,但在设计复杂的并发程序时,还需要考虑其他因素,如数据一致性、死锁、饥饿等问题。因此,在使用 std::atomic<T> 时,开发者需要仔细考虑其使用场景和并发需求,以确保程序的正确性和性能。
下面是一个简单的示例,分别使用原子整数与普通整数做递增:
cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> atomic_counter(0); // 初始化原子整数为0
int counter(0); // 初始化整数为0
void atomic_increment() {
for (int i = 0; i < 1000000; ++i) {
++atomic_counter; // 原子递增
}
}
void increment() {
for (int i = 0; i < 1000000; ++i) {
++counter; // 递增
}
}
int main()
{
std::thread t1(atomic_increment);
std::thread t2(atomic_increment);
t1.join();
t2.join();
std::thread t3(increment);
std::thread t4(increment);
t3.join();
t4.join();
std::cout << "Final atomic counter value: " << atomic_counter << std::endl;
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
上面代码的输出为:
Final atomic counter value: 2000000
Final counter value: 1300279
这个例子创建了一个原子整数 atomic_counter 与一个普通整数 counter,并分别在不同线程中对它们进行递增操作。由于 ++atomic_counter 是原子的,因此最终的计数值将是 2000000,即使两个线程同时执行递增操作。而 ++counter 不是原子的,所以最终的值不确定。
2.2 std::atomic_flag
std::atomic_flag 是 C++11 标准库中的一个类,它提供了一种最低级别的原子操作。这个类只能有两种状态:设置(true)或清除(false)。由于其简单性,std::atomic_flag 的操作通常比其他原子类型(如 std::atomic<T>)具有更高的性能。然而,std::atomic_flag 的功能非常有限,它不支持加载、存储或比较交换(compare-and-swap)等复合操作。
成员函数
- std::atomic_flag::clear():将原子标志清除为 false。
- std::atomic_flag::test_and_set():如果原子标志原来是 false,则将其设置为 true 并返回 false;如果原来是 true,则保持为 true 并返回 true。这是一个原子操作。
示例
下面是一个使用 std::atomic_flag 的简单示例,展示了如何使用它来实现一个简单的自旋锁(spinlock):
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void worker(int id) {
while (lock.test_and_set()) {
// 自旋等待,直到获取到锁
}
std::cout << "Worker " << id << " is working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
lock.clear(); // 释放锁
}
int main()
{
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}
上面代码的输出为:
Worker 1 is working...
Worker 2 is working...
这个示例定义了一个全局的 std::atomic_flag 对象 lock,用作自旋锁。在 worker 函数中,使用 lock.test_and_set() 来尝试获取锁。如果返回 false,表示成功获取到锁,可以执行临界区代码;如果返回 true,则表示锁已被其他线程持有,随即进入自旋等待,不断尝试获取锁,直到成功为止。执行完临界区代码后,使用 lock.clear() 来释放锁,允许其他线程获取锁并执行临界区代码。
2.3 std::atomic_bool
std::atomic_bool 是 C++11 标准库中的一个模板特化,它提供了对布尔类型值的原子操作。原子操作是那些在多线程环境中可以安全执行而不会被其他线程干扰的操作。std::atomic_bool 允许安全地在多线程环境中读取、写入或交换布尔值,而不需要额外的锁或其他同步机制。
std::atomic_bool 提供了一系列成员函数,允许进行以下操作:
- 构造与初始化:std::atomic_bool 可以用一个布尔值来初始化。
- load():从原子布尔值中读取当前的值。
- store():将一个新的值存储到原子布尔值中。
- exchange():以原子方式将当前值与提供的值进行交换,并返回旧值。
- compare_exchange_weak() 和 compare_exchange_strong():尝试以原子方式比较当前值与期望值,如果它们相等,则将新值存储到原子布尔值中,并返回是否成功比较并交换。
- operator=:赋值操作符,用于存储新值。
- operator bool() 和 operator T():类型转换操作符,允许将 std::atomic_bool 当作布尔值来使用(例如,在条件语句中)。
示例
下面是一个使用 std::atomic_bool 的示例,展示如何在多线程环境中安全地更新一个布尔标志:
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
std::atomic_bool flag(false); // 初始化一个原子布尔值为 false
void set_flag() {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟一些工作
flag.store(true); // 以原子方式设置标志为 true
}
void check_flag() {
while (!flag.load()) { // 以原子方式读取标志,如果为 false,则继续循环
std::cout << "Flag is not set, waiting...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待一段时间再检查
}
std::cout << "Flag is set!\n";
}
int main()
{
std::thread setter(set_flag);
std::thread checker(check_flag);
setter.join();
checker.join();
return 0;
}
上面代码的输出为:
Flag is not set, waiting...
Flag is not set, waiting...
Flag is not set, waiting...
Flag is not set, waiting...
Flag is not set, waiting...
Flag is set!
这个示例定义了一个 std::atomic_bool 对象 flag,初始化为 false。然后创建了两个线程:一个线程 setter 在一段时间后设置 flag 为 true,另一个线程 checker 不断检查 flag 的值,直到它变为 true。
set_flag 函数中使用了 flag.store(true) 来以原子方式设置 flag 的值。check_flag 函数中使用了 flag.load() 来以原子方式读取 flag 的值,并在一个循环中等待其变为 true。
通过这种方式,可以确保即使 setter 和 checker 线程同时运行,flag 的更新和读取操作也是线程安全的,不会引发数据竞争或其他并发问题。