一、原子操作简介
在多线程编程中,通常我们需要多个线程共享数据。但如果不加以控制,多个线程同时访问同一数据,可能会导致数据不一致或竞争条件(race conditions)。为了解决这个问题,我们引入了 原子操作。
原子操作 是指在执行时不会被中断、打断或修改的操作。换句话说,原子操作要么完全执行,要么完全不执行,不会在中间被其他线程的操作干扰。这种特性对于多线程环境中的数据访问是至关重要的。
C++11 引入了 <atomic>
库,它提供了对原子操作的支持。通过这个库,我们可以确保在多线程环境中对数据的访问不会导致竞争条件。
二、C++ 原子操作的基本概念
在 C++ 中,原子操作的关键是 原子类型(atomic types)。原子类型是由 std::atomic
模板类提供的,它包装了一个类型,确保对该类型的所有操作(如读取、写入、更新等)都是原子的。
关键概念
- 原子类型 (
std::atomic
):可以包装任何基本类型(如int
、bool
、float
等),并保证对这些类型的操作是原子的。 - 原子操作:对原子变量的操作是不可分割的,意味着在多线程中不会被打断。
- 原子变量:一个变量可以被声明为原子类型(如
std::atomic<int>
),它保证在多线程环境下对该变量的操作是安全的。
常用的原子操作
- load:读取原子变量的值。
- store:将一个值存储到原子变量中。
- exchange:将原子变量的值替换为一个新值,并返回旧值。
- compare_exchange_weak / compare_exchange_strong:原子地进行条件交换操作。若当前值等于预期值,则交换新值。
- fetch_add / fetch_sub:原子地执行加法或减法操作,并返回旧值。
三、std::atomic
类模板
std::atomic
是一个模板类,可以用于包装基础数据类型,确保对该数据类型的所有操作都是原子性的。
基本使用
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.push_back(std::thread(increment)); // 启动4个线程
}
for (auto& t : threads) {
t.join(); // 等待线程完成
}
cout << "Counter value: " << counter.load() << endl; // 输出最终结果
return 0;
}
常用原子操作的详细介绍
1. load() 和 store()
load()
:从原子变量读取值。store()
:将值存储到原子变量中。
std::atomic<int> a(10);
int value = a.load(); // 获取原子变量a的值
a.store(20); // 将原子变量a的值设置为20
2. exchange()
exchange
会将原子变量的值更新为给定的值,并返回原先的值。常用来在并发环境下进行“交换”操作。
std::atomic<int> a(5);
int old_value = a.exchange(10); // 将a的值设置为10,并返回旧值5
3. fetch_add() 和 fetch_sub()
fetch_add()
:执行原子加法操作,并返回旧值。fetch_sub()
:执行原子减法操作,并返回旧值。
std::atomic<int> a(5);
int old_value = a.fetch_add(1); // 将a加1,并返回旧值
// a的值现在是6
4. compare_exchange_weak() 和 compare_exchange_strong()
这两个函数会比较当前原子变量的值和预期值。如果相同,就将原子变量的值设置为新值。否则,操作失败并返回 false
。
compare_exchange_weak()
:若失败,可能会重新尝试,性能相对较高。compare_exchange_strong()
:若失败,会返回false
,并且不再尝试。
std::atomic<int> a(5);
int expected = 5;
if (a.compare_exchange_weak(expected, 10)) {
// 如果a的值是5,设置为10
std::cout << "Value changed!" << std::endl;
} else {
std::cout << "Value not changed!" << std::endl;
}
5. memory_order
std::atomic
的操作可以指定不同的内存顺序(memory ordering),控制不同线程之间的操作顺序。这对于高效并发编程非常重要。常见的内存顺序有:
memory_order_relaxed
:不保证其他线程与该线程的操作顺序。memory_order_consume
:保证后续操作依赖于当前操作。memory_order_acquire
:保证所有的读取操作不会在当前操作之前执行。memory_order_release
:保证所有的写操作不会在当前操作之后执行。memory_order_acq_rel
:同时保证 acquire 和 release。memory_order_seq_cst
:最强的内存顺序,保证所有操作的顺序一致。
应用场景
-
计数器:
原子计数器是一个典型的应用场景。多个线程可能会并发修改一个计数器,原子操作可以保证计数器值的一致性。 -
锁的实现:
一些简化版的锁(如自旋锁)可以使用原子操作来减少性能开销。 -
无锁数据结构:
通过原子操作可以设计一些高效的无锁数据结构(如无锁队列、栈等)。 -
状态标志:
原子布尔值常常用来作为状态标志,例如线程是否完成,任务是否已开始等。
好的,以下是两道练习题目,可以帮助你更好地理解和掌握 C++ 中的原子操作和多线程编程。
四、练习题目
练习题 1: 使用 std::atomic
实现线程安全计数器
题目要求
- 编写一个线程安全的计数器类,使用
std::atomic
来保证多个线程并发访问时的数据一致性。 - 设计一个类
Counter
,其中有一个原子整型成员变量value
。 - 提供
increment
(增加)和get_value
(获取当前值)两个方法。 - 创建多个线程并发地对计数器执行增加操作,最后输出计数器的最终值。
示例
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
class Counter {
private:
std::atomic<int> value;
public:
Counter() : value(0) {}
void increment() {
value.fetch_add(1, std::memory_order_relaxed);
}
int get_value() const {
return value.load(std::memory_order_relaxed);
}
};
void increment_counter(Counter& counter, int times) {
for (int i = 0; i < times; ++i) {
counter.increment();
}
}
int main() {
Counter counter;
// 创建多个线程并发增加计数器的值
vector<thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(thread(increment_counter, ref(counter), 1000));
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
cout << "Final counter value: " << counter.get_value() << endl;
return 0;
}
练习题 2: 使用 compare_exchange_strong
实现自旋锁
题目要求
- 编写一个简单的自旋锁类,使用
std::atomic<bool>
来实现锁的原子操作。 - 使用
compare_exchange_strong
来实现lock
和unlock
操作。 - 在
main
函数中启动多个线程,模拟多个线程访问共享资源的场景,确保通过自旋锁控制资源的访问。
示例
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
class SpinLock {
private:
std::atomic<bool> flag;
public:
SpinLock() : flag(false) {}
void lock() {
bool expected = false;
while (!flag.compare_exchange_strong(expected, true)) {
expected = false; // 如果失败,重新尝试
}
}
void unlock() {
flag.store(false, std::memory_order_release);
}
};
SpinLock spinlock;
void access_shared_resource(int thread_id) {
spinlock.lock();
cout << "Thread " << thread_id << " is accessing the shared resource." << endl;
this_thread::sleep_for(chrono::milliseconds(100)); // 模拟处理过程
cout << "Thread " << thread_id << " has finished accessing the resource." << endl;
spinlock.unlock();
}
int main() {
vector<thread> threads;
// 创建多个线程模拟并发访问共享资源
for (int i = 0; i < 5; ++i) {
threads.push_back(thread(access_shared_resource, i));
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
总结
- 练习题 1 帮助你练习如何使用
std::atomic
实现线程安全的计数器,理解原子操作对共享数据的保护作用。 - 练习题 2 让你掌握如何使用
compare_exchange_strong
来实现自旋锁,理解锁机制的基本原理。
五、进一步讨论
为什么不使用原子操作会导致统计混乱:
假设有两个线程同时执行如下代码:
g_count = g_count + 1;
在没有原子操作的情况下,可能会发生以下步骤:
- 线程 A 读取
g_count
的值,假设为 5。 - 线程 B 也读取
g_count
的值,仍然是 5。 - 线程 A 对
g_count
进行加 1 操作,得到新的值 6。 - 线程 B 也对
g_count
进行加 1 操作,得到新的值 6。
最终的 g_count
值应该是 7(5 + 1 + 1),但实际上由于没有原子操作,线程 A 和线程 B 的加法操作是并行进行的,导致最终结果错误(g_count
被错误地更新为 6)。
解决方法: 使用原子操作,如 std::atomic
来确保这些操作是 原子的,即每次修改操作都是不可分割的,避免数据竞争。
支持的原子操作:
原子操作一般支持的操作包括:
- 基本算术操作:
++
,--
,+=
,-=
,&=
,|=
,^=
等。 - 这些操作是原子的,也就是说,当多个线程同时对
std::atomic<int>
进行增减或位操作时,编译器会确保每次操作是不可分割的。
示例
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;
atomic<int> g_count = 0;
void mythread1() {
for (int i = 0; i < 1000000; i++) {
g_count++;
}
}
int main() {
std::thread t1(mythread1);
std::thread t2(mythread1);
t1.join();
t2.join();
cout << "正常情况下结果应该是2000000次,实际是" << g_count << endl;
}
不支持的操作:
并不是所有的操作都可以直接在原子变量上执行。例如,g_count = g_count + 1;
这种方式 不行,因为它首先会读取 g_count
,然后执行加法并将结果写回 g_count
,这个过程并不是原子操作。如果多个线程同时进行这种操作,可能会导致竞态条件,结果不正确。
为什么 g_count = g_count + 1;
不行:
-
g_count = g_count + 1;
这个表达式分为两步:- 读取
g_count
的值。 - 将计算出的新值写回
g_count
。
如果在这两步之间,另一个线程也读取并修改了
g_count
的值,最终可能导致结果错误。 - 读取
-
原子操作(如
fetch_add
、fetch_sub
等)可以保证这类操作是 不可分割 的,避免中间被打断。通过原子操作,线程会依次执行加法或减法操作,确保每次修改都正确地反映到内存中。
对自己进行操作:
一些原子类型(如 std::atomic<int>
)只支持对自身进行修改的操作。这意味着,如果要执行某些复杂的表达式(例如 g_count = g_count + 1;
),需要使用更复杂的原子操作(如 fetch_add
),而不能直接使用加法或赋值操作符。
示例:
std::atomic<int> g_count(0);
// 错误:不支持直接执行 g_count = g_count + 1;
g_count = g_count + 1; // 编译错误
// 正确:使用原子操作
g_count.fetch_add(1, std::memory_order_relaxed); // 原子增加1
g_count++;