C++ 多线程库 入门指南
在现代编程中,多任务处理早已不是“高级功能”,而是开发高性能应用的标配。无论是图像处理、网络请求,还是数据计算,单线程程序常常因为“卡顿”或“效率低下”而难以满足需求。C++ 从 C++ 11 起正式引入了标准的多线程支持,其中最核心的组件就是 <thread> 库。它为我们提供了一种跨平台、安全且高效的线程管理方式。
如果你正在学习 C++,或者已经掌握基础语法,但对并发编程感到陌生,那么这篇教程将带你一步步理解 C++ 多线程库
创建与启动线程
在 C++ 中,std::thread 是创建线程的入口类。它就像一个“线程工厂”,你告诉它“我要执行什么函数”,它就会为你创建一个独立的执行流。
下面是一个最简单的例子:
#include <iostream>
#include <thread>
// 定义一个普通的函数,作为线程的入口
void print_hello() {
for (int i = 0; i < 5; ++i) {
std::cout << "Hello from thread " << i << std::endl;
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
// 创建一个线程对象,传入函数名作为参数
std::thread t(print_hello);
// 主线程继续执行
std::cout << "Main thread is running..." << std::endl;
// 等待子线程结束,防止主程序提前退出
t.join();
std::cout << "All threads finished." << std::endl;
return 0;
}
代码注释说明:
std::thread t(print_hello);:创建一个新线程,执行print_hello函数。注意这里只传函数名,不加括号,否则会直接调用。std::this_thread::sleep_for(...):让当前线程暂停指定时间,常用于模拟任务耗时,避免输出过快。t.join();:主线程等待子线程结束。这是关键!如果没有join(),主程序可能在子线程完成前就退出,导致子线程未执行完毕就被强制终止。
⚠️ 提示:
join()不能重复调用,否则程序会崩溃。如果不需要等待,可以使用detach(),但要小心资源泄漏。
线程间传递参数
很多时候,我们希望线程执行时能接收参数。std::thread 支持传递任意类型参数,包括基本类型、结构体、甚至智能指针。
#include <iostream>
#include <thread>
#include <string>
// 接收两个参数的函数
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (iteration " << i + 1 << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
int main() {
// 传递字符串和整数参数
std::thread t(greet, "Alice", 3);
std::cout << "Main thread continues..." << std::endl;
// 等待线程完成
t.join();
std::cout << "Greeting thread finished." << std::endl;
return 0;
}
关键点:
std::thread t(greet, "Alice", 3);:参数通过值传递,"Alice"和3会被复制到子线程中。- 如果你希望传递引用,必须使用
std::ref()包装,否则会出错。
int counter = 0;
void increment(int& count) {
for (int i = 0; i < 5; ++i) {
++count;
std::cout << "Count: " << count << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread t(increment, std::ref(counter)); // 使用 std::ref 传递引用
t.join();
std::cout << "Final count: " << counter << std::endl;
return 0;
}
💡 小贴士:
std::ref()是一个工具函数,用于包装引用,让线程能共享变量。如果不加,编译器会报错,因为引用不能被复制。
线程生命周期管理:join 与 detach
线程一旦创建,就拥有自己的生命周期。我们有两种方式来管理它:
| 方法 | 说明 | 适用场景 |
|---|---|---|
join() |
等待线程结束,阻塞当前线程 | 主线程需要等待子线程完成 |
detach() |
让线程独立运行,脱离主线程控制 | 子线程执行后台任务,无需等待 |
使用 detach() 时必须特别小心,因为一旦线程脱离控制,你将无法再调用 join(),也无法保证它安全退出。
#include <iostream>
#include <thread>
void background_task() {
for (int i = 0; i < 10; ++i) {
std::cout << "Background task: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
std::cout << "Background task finished." << std::endl;
}
int main() {
std::thread t(background_task);
// 让线程脱离主线程
t.detach();
std::cout << "Main thread continues without waiting." << std::endl;
// 主线程退出,子线程仍会继续运行
// 注意:这里不能调用 t.join(),否则程序崩溃
// 等待一段时间,确保后台任务完成
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
⚠️ 危险警告:如果
main()提前退出,而detach()的线程尚未完成,程序可能异常终止。建议在detach()后添加适当的等待机制或使用 RAII 模式管理。
线程唯一性与资源安全
每个 std::thread 对象都唯一对应一个线程。这意味着你不能复制 std::thread 对象,但可以移动。
#include <iostream>
#include <thread>
void worker() {
std::cout << "Worker thread running." << std::endl;
}
int main() {
std::thread t1(worker);
// 错误:不能复制 thread
// std::thread t2 = t1; // 编译错误!
// 正确:使用移动语义
std::thread t2 = std::move(t1);
// t1 现在无效,不能再 join 或 detach
if (t1.joinable()) {
t1.join();
} else {
std::cout << "t1 is not joinable after move." << std::endl;
}
t2.join();
return 0;
}
关键概念:
joinable():判断线程是否可以被join()或detach()。std::move():将线程的所有权从一个对象转移到另一个,是唯一合法的线程转移方式。
比喻:
std::thread就像一个“许可证”,一个许可证只能给一个线程使用。复制相当于“伪造许可证”,是非法的;而移动,就像“转让许可证”,是合法且安全的。
线程安全与共享数据保护
当多个线程访问同一个变量时,就可能出现“竞态条件”(Race Condition)。例如两个线程同时对 counter 加 1,结果可能只加了 1 次。
#include <iostream>
#include <thread>
#include <vector>
int counter = 0;
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter; // 竞态条件!
}
}
int main() {
std::vector<std::thread> threads;
// 创建 10 个线程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
运行结果可能是 9999 或 9000,而不是预期的 10000。这是由于多个线程同时读写 counter,导致操作被覆盖。
解决方案:使用互斥锁(std::mutex)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx; // 互斥锁
void increment() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁/解锁
++counter;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl; // 输出 10000
return 0;
}
核心思想:
std::lock_guard是一个 RAII 机制,进入作用域时自动加锁,离开时自动解锁。- 任何时刻只有一个线程能持有锁,从而保证
++counter操作的原子性。
总结:C++ 多线程库 的核心价值
C++ 多线程库
对于初学者,建议从 std::thread + join() 开始,逐步理解线程的执行流程;中级开发者则应深入学习 std::mutex、std::condition_variable 等同步原语,以构建更复杂的并发模型。
记住:多线程不是“让程序跑得更快”的万能药,而是需要精心设计的工具。错误的并发使用,反而会导致性能下降、死锁甚至崩溃。
掌握 C++ 多线程库