C++多线程的简单入门
C++11 引入了标准线程库,使得多线程编程变得更加简单和可移植。下面是一个快速入门指南,帮助你理解C++多线程编程的基本概念。
基本概念:
线程:程序中的执行流
并发:多个任务交替执行(单核CPU)
并行:多个任务同时执行(多核CPU)
创建线程(thread)
想象一下你在厨房做饭(主线程),同时让助手去超市买调料(新线程):
创建线程:你对助手说"去超市买酱油"
join():你站在厨房门口等待,直到助手回来才继续做饭
不join():你不等助手回来,继续做其他事情,但可能会遇到问题 - 比如需要酱油时助手还没回来
#include <iostream>
#include <thread>
#include <chrono>
// 助手线程的任务:模拟去超市买东西
void goToSupermarket(int timeNeeded) {
std::cout << "助手: 开始去超市,需要 " << timeNeeded << " 秒" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(timeNeeded));
std::cout << "助手: 买完东西回来了!" << std::endl;
}
int main() {
std::cout << "主厨: 开始准备晚餐" << std::endl;
// 创建助手线程(让助手去买东西)
std::thread assistant(goToSupermarket, 3);
std::cout << "主厨: 助手已出发,我可以先做其他准备工作..." << std::endl;
// join() 的含义:主厨等待助手回来
std::cout << "主厨: 等待助手回来..." << std::endl;
assistant.join(); // 这里主线程(主厨)会阻塞,直到助手线程完成
// 只有在助手回来后,才会执行下面的代码
std::cout << "主厨: 助手回来了,现在可以用买来的调料继续做菜!" << std::endl;
std::cout << "主厨: 晚餐完成!" << std::endl;
return 0;
}
'''
输出结果:
主厨: 开始准备晚餐
助手: 开始去超市,需要 3 秒
主厨: 助手已出发,我可以先做其他准备工作...
主厨: 等待助手回来...
助手: 买完东西回来了!
主厨: 助手回来了,现在可以用买来的调料继续做菜!
主厨: 晚餐完成!
'''
基本用法总结:
std::thread 构造函数接受一个可调用对象和其参数,线程在构造后立即开始执行。
join() 方法阻塞当前线程,直到目标线程执行完成
互斥量(mutex)
互斥量 是“互斥锁”的简称,用于保护共享数据,防止多个线程同时访问。因为在多线程环境中,如果多个线程不加控制地同时读写同一块数据,会导致结果不可预测。
可以把它想象成一个卫生间的门,门上有一把钥匙(互斥量)。这个卫生间(共享资源)一次只能有一个人(线程)使用。流程是这样的:
一个人来了,拿到钥匙(lock),进入卫生间,并锁上门。
其他想来用卫生间的人发现钥匙没了(被lock了),就必须等待(被阻塞)。
里面的人用完了,出来并把钥匙放回原处(unlock)。
等待的人中有一个可以拿到钥匙,然后进入。
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx; // 定义一个全局的互斥量
void safe_increment() {
for (int i = 0; i < 1000000; ++i) {
mtx.lock(); // 上锁:进入临界区
counter++; // 现在这个操作是安全的了
mtx.unlock(); // 解锁:离开临界区
}
}
int main() {
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "Counter value: " << counter << std::endl; // 结果必定是 2000000
return 0;
}
但是,手动调用 lock() 和 unlock() 非常危险!如果在临界区中发生异常或提前返回,unlock() 可能不会被调用,导致死锁(其他线程永远在等待)。
C++ 遵循 RAII 原则,提供了几个锁管理器类,它们在构造时自动上锁,在析构时自动解锁,确保异常安全。
RAII 是 Resource Acquisition Is Initialization 的缩写,中文直译为“资源获取即初始化”。RAII 用 C++ 对象析构函数一定会被执行的特性,来确保资源一定会被安全地释放。利
'''
1. std::lock_guard (C++11)
最简单的锁管理器,在构造时上锁,析构时解锁。不支持手动解锁。
'''
void safe_increment() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 构造函数中调用 mtx.lock()
counter++;
// lock 析构时自动调用 mtx.unlock()
}
}
'''
2. std::unique_lock (C++11)
更灵活的锁管理器。除了 std::lock_guard 的功能外,还支持:
延迟上锁:构造时不立即上锁。
手动解锁和重新上锁。
移动语义。
'''
void safe_increment() {
for (int i = 0; i < 1000000; ++i) {
std::unique_lock<std::mutex> ulock(mtx, std::defer_lock); // 延迟上锁
// ... 这里可以执行一些不需要锁的操作 ...
ulock.lock(); // 现在才上锁
counter++;
ulock.unlock(); // 可以手动提前解锁
// ... 这里可以执行一些不需要锁的操作 ...
// ulock 析构时,如果还持有锁,会自动解锁
}
}
条件变量 (condition variable)
条件变量,允许一个或多个线程等待,直到另一个线程修改了共享数据(即某个“条件”满足)并通知条件变量。
简单来说,它就是线程间进行通信的一种机制,用于一个线程等待特定事件发生,而另一个线程在事件发生后通知等待的线程。
想象一个经典的生产者-消费者场景:
生产者线程:向一个队列(共享数据)中放入数据。
消费者线程:从队列中取出数据。
如果没有条件变量,消费者线程可能需要不断地循环检查队列是否为空(“忙等待”或“自旋”),这非常浪费 CPU 资源。
条件变量解决了这个问题:它让消费者线程在队列为空时进入“等待”状态,主动释放 CPU。当生产者向队列放入数据后,再“通知”消费者,消费者被唤醒并从等待处继续执行。这大大提高了效率。
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
std::queue<int> g_queue; // 共享队列
std::mutex g_mutex; // 保护队列的互斥锁
std::condition_variable g_cv; // 条件变量
void producer() {
for (int i = 0; i < 10; ++i) {
{
// 操作共享数据前加锁
std::unique_lock<std::mutex> lock(g_mutex);
g_queue.push(i);
std::cout << "Produced: " << i << std::endl;
} // 这里 lock 超出作用域,自动释放锁
// 通知一个等待的消费者
g_cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(g_mutex);
// 等待条件满足(队列非空)
// 使用带谓词的 wait,防止虚假唤醒
// 谓词 (Predicate) 是什么?
//在 condition_variable::wait 的语境下,谓词是一个可调用对象
//(比如函数、lambda 表达式、函数对象),它返回 true 或 false。
//它的唯一职责就是检查我们真正等待的“条件”是否已经满足。 它通常
//是一个简单的检查语句,访问被互斥锁保护的共享数据,并返回一个布
//尔值。核心思想: 我们等待的并不是条件变量本身,而是由谓词定义的
//某个特定条件变为真。条件变量只是实现这个等待过程的机制。
// lambda 函数 `[&](){ return !g_queue.empty(); }` 就是谓词 (Predicate)
g_cv.wait(lock, [&]() {
return !g_queue.empty();
});
// 走到这里,锁已经被重新获取,并且队列肯定不为空
int data = g_queue.front();
g_queue.pop();
std::cout << "Consumed: " << data << std::endl;
// 如果收到结束信号(例如数据为-1),可以退出循环
// if (data == -1) break;
}
}
int main() {
std::thread prod_thread(producer);
std::thread cons_thread(consumer);
prod_thread.join();
cons_thread.join();
return 0;
}
原子操作(atomic operation)
原子操作提供无锁的线程安全操作,适用于简单的计数器等场景。
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> atomicCounter(0);
int nonAtomicCounter = 0;
void incrementAtomic() {
for (int i = 0; i < 100000; ++i) {
++atomicCounter;
}
}
void incrementNonAtomic() {
for (int i = 0; i < 100000; ++i) {
++nonAtomicCounter;
}
}
int main() {
// 测试原子操作
std::vector<std::thread> atomicThreads;
for (int i = 0; i < 10; ++i) {
atomicThreads.emplace_back(incrementAtomic);
}
for (auto& t : atomicThreads) {
t.join();
}
// 测试非原子操作(结果不确定)
std::vector<std::thread> nonAtomicThreads;
for (int i = 0; i < 10; ++i) {
nonAtomicThreads.emplace_back(incrementNonAtomic);
}
for (auto& t : nonAtomicThreads) {
t.join();
}
std::cout << "Atomic counter: " << atomicCounter << std::endl;
std::cout << "Non-atomic counter: " << nonAtomicCounter << std::endl;
return 0;
}
异步操作(asynchronous operation)
异步操作,允许你启动一个任务后,无需等待其完成,就可以继续执行后续的代码。当需要该任务的结果时,再去获取它。这是一种提高程序性能和响应能力的关键技术,特别是在需要处理耗时操作(如I/O读写、网络请求、复杂计算)时。
使用异步操作:
你告诉系统:“请在未来某个时候帮我执行 CalculateHugeData() 这个任务”。
系统说:“好的,这是你的‘凭证’(future),你先去忙别的吧”。
你的主程序可以继续流畅运行,响应用户操作、更新UI等。
当你真正需要那个计算结果时,你拿出之前的“凭证”问:“任务完成了吗?把结果给我”。如果完成了就直接拿到结果;如果还没完成,你可以选择等待或稍后再试。
#include <iostream>
#include <future> // 核心头文件
#include <chrono>
#include <thread>
// 一个耗时的函数
int CalculateSomething(int x, int y) {
// 假设这个计算很耗时
std::this_thread::sleep_for(std::chrono::seconds(2));
return x + y;
}
int main() {
// 使用 std::async 异步地启动 CalculateSomething 任务
// std::launch::async 策略表示一定要在新线程中执行
std::future<int> result_future = std::async(std::launch::async, CalculateSomething, 10, 20);
// 主线程(当前线程)不会阻塞,可以继续做其他工作
std::cout << "主线程正在处理其他任务..." << std::endl;
// 模拟主线程也做一些工作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程其他工作做完了,现在需要异步计算的结果了。" << std::endl;
// 使用 get() 获取异步操作的结果。
// 如果异步计算还没完成,get() 会阻塞当前线程,直到结果就绪。
int result = result_future.get();
std::cout << "计算结果是: " << result << std::endl; // 输出 30
return 0;
}
'''
结果输出:
主线程正在处理其他任务...
(等待约1秒...)
主线程其他工作做完了,现在需要异步计算的结果了。
(再等待约1秒,因为异步任务总共需要2秒,此时已过去1秒,还差1秒)
计算结果是: 30
'''更多推荐



所有评论(0)