C++ 多线程:从零开始掌握并发编程
你有没有遇到过这样的场景:程序在处理一个大任务时,界面卡住不动,用户只能干等?或者,某个计算密集型任务耗时太久,影响了整体响应速度?这时候,你就该考虑使用 C++ 多线程了。
C++ 多线程是一种让程序同时执行多个任务的技术,它能显著提升程序效率,尤其在处理 I/O 操作、数据计算、图像渲染等场景中表现突出。想象一下,你正在做一桌饭菜,如果只靠一个人炒菜、煮饭、切菜,那得花很久。但如果你能同时让一个人炒菜,另一个人煮饭,再一个人切菜,效率自然翻倍。C++ 多线程,就是给程序“分身术”——让多个“小人”同时干活。
今天我们就从基础开始,一步步带你走进 C++ 多线程的世界,不讲虚的,只讲能用、好懂、实用的内容。
为什么需要 C++ 多线程?
在传统的单线程程序中,代码是按顺序一条一条执行的。比如你写了一个函数计算 1 到 1000000 的和,程序会从 1 加到 1000000,期间其他任务无法执行。如果这个计算耗时 5 秒,那用户就得等 5 秒,这显然不理想。
而 C++ 多线程允许你将一个大任务拆分成多个子任务,让它们在不同的线程中并行执行。比如你可以用一个线程计算前 50 万的和,另一个线程计算后 50 万的和,最后合并结果。这样整体时间可能只用 3 秒,效率提升明显。
更进一步,C++ 多线程还能让程序“一边干活一边响应”——比如在后台下载文件的同时,界面仍然可以滚动、点击按钮。这种体验,正是现代软件的标配。
创建线程的两种方式
C++11 引入了标准的线程库 <thread>,这是现代 C++ 多线程编程的基础。我们先来认识两种创建线程的方法。
使用函数作为线程入口
#include <iostream>
#include <thread>
#include <chrono>
// 定义一个普通函数,作为线程执行的入口
void print_message() {
for (int i = 0; i < 5; ++i) {
std::cout << "线程 ID: " << std::this_thread::get_id()
<< " 输出: Hello from thread!" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
int main() {
// 创建一个线程对象,传入函数名作为入口
std::thread t1(print_message);
// 主线程继续执行
std::cout << "主线程在运行..." << std::endl;
// 等待 t1 线程执行完毕
t1.join();
std::cout << "所有线程已结束。" << std::endl;
return 0;
}
代码注释:
std::thread t1(print_message);:创建一个线程,让它执行print_message函数。std::this_thread::get_id():获取当前线程的唯一 ID,用于区分不同线程。std::this_thread::sleep_for(...):让当前线程暂停指定时间,模拟任务耗时。t1.join():阻塞主线程,直到线程 t1 完成。这是关键!必须调用join(),否则程序可能崩溃。
使用 Lambda 表达式创建线程
Lambda 表达式是 C++11 的另一个强大特性,它能让线程执行匿名函数。
#include <iostream>
#include <thread>
#include <chrono>
int main() {
// 使用 Lambda 表达式创建线程
std::thread t2([]() {
for (int i = 0; i < 3; ++i) {
std::cout << "Lambda 线程 ID: " << std::this_thread::get_id()
<< " 输出: I'm a lambda thread!" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
});
std::cout << "主线程正在运行..." << std::endl;
// 等待线程完成
t2.join();
std::cout << "Lambda 线程已结束。" << std::endl;
return 0;
}
代码注释:
[]()是 Lambda 的语法,[]表示不捕获外部变量,括号内是参数列表(空表示无参数)。- Lambda 内部可以写任意逻辑,非常灵活,适合小型任务。
- 与函数方式一样,必须调用
join(),否则资源泄漏。
线程生命周期管理:join 与 detach
线程创建后,必须明确它的“去向”。C++ 提供了两种方式:join 和 detach。
| 方法 | 作用 | 使用场景 |
|---|---|---|
join() |
阻塞当前线程,直到目标线程完成 | 你希望等待线程结束再继续执行 |
detach() |
让线程独立运行,脱离主线程控制 | 线程执行后台任务,无需等待 |
join 的使用场景
std::thread t3([]() {
std::cout << "任务开始..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "任务完成。" << std::endl;
});
// 主线程等待 t3 完成
t3.join();
std::cout << "主线程继续执行。" << std::endl;
说明:
join()会阻塞主线程,直到t3执行完毕。适用于需要等待结果的场景。
detach 的使用场景
std::thread t4([]() {
for (int i = 0; i < 5; ++i) {
std::cout << "后台线程: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
});
// 让线程脱离主线程,独立运行
t4.detach();
std::cout << "主线程继续运行,不等待 t4。" << std::endl;
// 注意:不能再调用 t4.join() 或 t4.detach(),否则程序崩溃
重要提醒:一旦调用
detach(),就不能再调用join()。如果线程还没结束就被销毁,程序会崩溃。所以使用detach时要格外小心。
共享数据与线程安全
多个线程同时访问同一个变量,就可能出问题。这就像几个人同时在一张纸上写字,写到最后谁也看不懂。
问题示例:竞态条件(Race Condition)
#include <iostream>
#include <thread>
#include <vector>
int counter = 0; // 全局共享变量
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 这里不是原子操作!
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "最终计数: " << counter << std::endl;
return 0;
}
问题分析:
++counter实际上包含三个步骤:读取 counter 值、加 1、写回。如果两个线程同时读取,可能都读到 0,然后都加 1,最终结果是 1 而不是 2。这就是竞态条件。
解决方案:使用互斥锁(mutex)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx; // 互斥锁,用于保护共享资源
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁,作用域结束自动解锁
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "最终计数: " << counter << std::endl; // 输出应为 200000
return 0;
}
代码注释:
std::lock_guard<std::mutex>是 RAII 机制,进入作用域自动加锁,离开时自动解锁。mtx是互斥锁对象,任何时刻只能有一个线程持有锁。- 使用
lock_guard可避免忘记解锁导致死锁。
线程池:高效管理线程资源
频繁创建和销毁线程开销大。线程池是一种预先创建多个线程,任务提交后由池中线程处理的模式。
虽然 C++ 标准库没有内置线程池,但我们可以简单实现一个:
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
explicit ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task(); // 执行任务
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for (std::thread& worker : workers) {
worker.join();
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};
int main() {
ThreadPool pool(4); // 创建 4 个线程的线程池
for (int i = 0; i < 10; ++i) {
pool.enqueue([i] {
std::cout << "任务 " << i << " 由线程 ID: " << std::this_thread::get_id()
<< " 执行。" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}
std::cout << "所有任务已提交。" << std::endl;
return 0;
}
说明:
- 线程池避免了频繁创建线程的开销。
- 使用
std::condition_variable实现线程间通信,任务就绪时唤醒等待线程。std::forward保证完美转发,支持各类任务。
总结与建议
C++ 多线程是提升程序性能的重要工具。从创建线程、管理生命周期,到处理共享数据和构建线程池,每一步都值得深入理解。
- 初学者建议从
std::thread+join开始,理解基本流程。 - 中级开发者应掌握
mutex和lock_guard,避免竞态条件。 - 高级应用可考虑线程池、任务队列等模式,提升系统稳定性。
记住:线程不是越多越好,过多线程反而会因上下文切换带来性能下降。合理设计、适度使用,才能真正发挥 C++ 多线程的优势。
在实际项目中,C++ 多线程能让你的程序更快、更流畅、更专业。现在就开始动手写吧,让代码“跑起来”!