【链接】cplusplus.com/reference/multithreading/
C++ 标准库提供了丰富的多线程支持,主要集中在 <thread>, <mutex>, <condition_variable>, 和 <future> 这些头文件中。
- std::thread类:用于创建和管理线程的对象。
- std::mutex类:用于实现互斥访问,保护共享资源的完整性。
- std::condition_variable类:用于线程间的条件同步。
- std::atomic模板类:用于实现原子操作,确保数据的原子性
线程管理
<thread>:用于创建和管理线程。使用 std::thread
类来创建线程。std::thread
的构造函数可以接受一个可调用对象(如函数、lambda表达式、函数对象等)以及该可调用对象所需的参数
#include <iostream>
#include <thread>
void threadFunction(int x) {
std::cout << "Hello from thread with argument: " << x << std::endl;
}
int main() {
std::thread t(threadFunction, 42); // 创建并启动线程
t.join(); // 等待线程结束
return 0;
}
int main() {
std::thread t([](){
std::cout << "Hello from a thread!" << std::endl;
});
t.join(); // 等待线程结束
return 0;
}
在C++中使用多线程时,除了直接传递函数指针给 std::thread 来创建新线程外,还可以使用函数对象(即实现了 operator() 的类实例)。函数对象不仅允许你传递参数给线程函数,还能保持状态,这为编写更复杂的并发程序提供了极大的灵活性
#include <iostream>
#include <thread>
//带有状态的函数对象: 这个函数对象在构造时接收一个参数,并在被调用时打印该参数
class PrintTask {
private:
int id;
public:
PrintTask(int thread_id) : id(thread_id) {} // 构造函数初始化id
void operator()() const {
std::cout << "Thread ID " << id << " is running.\n";
}
};
int main() {
PrintTask task1(1), task2(2);
std::thread t1(task1), t2(task2); // 创建两个线程,每个都有自己的任务和ID
t1.join();
t2.join();
return 0;
}
std::thread
提供了一些方法来管理线程:
join()
:等待线程结束。如果主线程调用了join()
,它将阻塞,直到与之关联的线程执行完毕。
detach()
:将线程与std::thread
对象分离,允许线程独立执行。一旦分离,std::thread
对象就不能再用来调用join()
或detach()
。
get_id()
:返回线程的ID。
swap()
:交换两个std::thread
对象的状态。
native_handle()
:返回底层实现特定的句柄。通常用于与平台相关的API交互。
互斥量
<mutex>:提供互斥量类型以保护共享数据不被同时访问
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 定义一个互斥量
void printBlock(int n, char c) {
std::lock_guard<std::mutex> lock(mtx); // 自动管理锁的获取和释放
for (int i = 0; i<n; ++i) {
std::cout << c;
}
std::cout << '\n';
}
int main(){
std::thread th1(printBlock, 50, '*');
std::thread th2(printBlock, 50, '$');
th1.join();
th2.join();
return 0;
}
条件变量
<condition_variable>:允许线程等待某个条件变为真, std::condition_variable
是C++标准库中的一个类,用于在多线程编程中实现线程间的条件变量和线程同步。它提供了等待和通知的机制,使得线程可以等待某个条件成立时被唤醒,或者在满足某个条件时通知其他等待的线程。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void printId(int id) {
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, []{return ready;}); // 等待直到ready为true
std::cout << "Thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all(); // 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i<10; ++i)
threads[i] = std::thread(printId,i);
std::cout << "10 threads ready to race...\n";
go(); // 触发所有线程继续执行
for (auto& th : threads) th.join();
return 0;
}
异步操作
<future>:用于处理异步任务的结果,std::future 是 C++ 标准库中用于处理异步操作结果的一个模板类,它与 std::async、std::promise 和 std::packaged_task 一起工作,提供了一种方便的方式来获取异步任务的执行结果。通过 std::future,可以在一个地方启动异步操作,并在另一个地方获取该操作的结果。
std::async: 启动一个异步任务并返回一个 std::future 对象。
std::future: 提供对异步结果的访问。
std::promise: 允许在某一时刻设置共享状态的值,然后通过关联的 std::future 对象来获取这个值。
std::packaged_task: 将可调用对象(如函数、lambda表达式或函数对象)包装成一个异步任务,并且可以将结果存储在一个 std::future 中。
#include <iostream>
#include <future>
#include <chrono>
// 模拟耗时操作的函数
int longComputation(int x) {
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟延迟
return x * x;
}
int main() {
// 使用 std::async 启动异步任务,并获得一个 std::future 对象
std::future<int> result = std::async(std::launch::async, longComputation, 5);
std::cout << "Doing other work...\n";
// 调用 get 方法等待任务完成并获取结果
int value = result.get();
std::cout << "The result is: " << value << "\n"; // 输出:The result is: 25
return 0;
}
使用 std::promise 设置值,并通过 std::future 获取该值
#include <iostream>
#include <thread>
#include <future>
void modifyValue(std::promise<int>&& p) {
int newValue = 42;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
p.set_value(newValue); // 设置共享状态的值
}
int main() {
std::promise<int> p;
std::future<int> f = p.get_future(); // 获取与 promise 相关联的 future
std::thread t(modifyValue, std::move(p));
// 等待 future 变得可用,并获取结果
int value = f.get();
std::cout << "Value: " << value << "\n"; // 输出:Value: 42
t.join();
return 0;
}
std::packaged_task 包装了一个可调用对象,使得它可以被调用多次,并且每次调用都可以生成一个新的 std::future 对象。
#include <iostream>
#include <future>
#include <thread>
int compute(int a, int b) {
return a + b;
}
int main() {
std::packaged_task<int(int, int)> task(compute); // 创建 packaged_task
std::future<int> result = task.get_future(); // 获取 future
std::thread t(std::move(task), 2, 3); // 移动 task 到新线程中
// 获取异步操作的结果
std::cout << "Result: " << result.get() << "\n"; // 输出:Result: 5
t.join();
return 0;
}
原子操作
std::atomic
是 C++11 引入的标准库模板类,用于提供原子操作支持。它允许你安全地对单个对象进行读写操作,即使在多线程环境下也是如此,而无需使用互斥锁等同步机制。原子操作是不可分割的操作,意味着它们不会被线程切换或中断打断,这使得它们非常适合用于实现无锁数据结构或简单的同步原语。
原子性: 操作要么完全执行,要么完全不执行,不会有中间状态。
内存顺序: 控制编译器和CPU如何优化指令的顺序,以保证多线程环境下的正确性。C++ 提供了几种内存顺序模型,包括 memory_order_relaxed, memory_order_acquire, memory_order_release, memory_order_acq_rel, 和 memory_order_seq_cst(默认)。std::atomic 提供了一种简单而有效的方法来确保共享数据的线程安全性。通过选择适当的内存顺序,可以根据性能需求和程序正确性要求调整原子操作的行为。虽然std::atomic 非常强大,但在设计复杂的并发程序时,仍需谨慎考虑死锁、活跃性以及其他并发相关的问题。
简单计数器
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0); // 定义一个原子类型的计数器
void incrementCounter() {
for (int i = 0; i < 1000; ++i) {
++counter; // 原子递增操作
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << "\n"; // 输出:Final counter value: 2000
}
内存顺序
使用不同的内存顺序来控制原子操作的行为。这里使用了 memory_order_relaxed,这意味着没有同步或顺序约束,适用于不需要同步其他变量的情况
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
std::atomic<bool> ready(false);
std::atomic<int> data(0);
void producer() {
data.store(42, std::memory_order_relaxed); // 存储值,但不与其它操作同步
ready.store(true, std::memory_order_release); // 发布操作,通知消费者可以继续
}
void consumer() {
while (!ready.load(std::memory_order_acquire)); // 等待直到ready变为true
// 当我们到达这里时,我们知道data已经被生产者设置好了
std::cout << "Data is " << data.load(std::memory_order_relaxed) << "\n";
}
int main() {
std::thread p(producer);
std::thread c(consumer);
p.join();
c.join();
return 0;
}
在这个例子中,memory_order_acquire 和 memory_order_release 确保了在消费者看到 ready 变为 true 后,能够安全地读取 data 的值。
使用自定义类型
虽然 std::atomic 最常用于基本类型,但它也可以用于自定义类型,只要这些类型满足可复制构造、可复制赋值、非浮点类型以及具有平凡的构造函数和析构函数等条件。
#include <atomic>
#include <iostream>
struct Point {
int x, y;
};
int main() {
std::atomic<Point> atomicPoint({0, 0});
Point initial = atomicPoint.load(); // 加载原子值
std::cout << "Initial point: (" << initial.x << ", " << initial.y << ")\n";
Point newValue = {5, 10};
atomicPoint.store(newValue); // 存储新值
Point current = atomicPoint.load();
std::cout << "Updated point: (" << current.x << ", " << current.y << ")\n";
return 0;
}