C++11 之前,多线程编程依赖平台特定 API(如 POSIX 的 pthread 或 Windows API),跨平台性差且管理繁琐。C++11 引入的 <thread> 库彻底改变了这一现状,它基于 RAII 机制 ,提供了跨平台的线程抽象,核心是将线程生命周期与对象生命周期绑定,同时配合同步原语(如 mutex)解决数据竞争问题。本文将从基础用法、生命周期管理、参数传递、同步配合到常见陷阱,系统拆解 std::thread 的核心知识点。
一、std::thread 基础:创建与启动线程
1.1 核心特性
std::thread 是线程的 "句柄类",对象创建后立即启动线程 (无需显式 start()),支持传入任意可调用对象(普通函数、lambda、函数对象、成员函数等)作为线程入口。
1.2 基本用法(带详细注释)
cpp
#include <iostream>
#include <thread> // 线程库头文件
#include <string>
// 1. 普通函数作为线程入口
void print_message(const std::string& msg) {
std::cout << "[线程] 消息: " << msg << "\n";
}
// 2. 函数对象(仿函数)作为线程入口
class Counter {
public:
void operator()(int n) const {
for (int i = 0; i < n; ++i) {
std::cout << "[线程] 计数: " << i << "\n";
}
}
};
int main() {
std::cout << "[主线程] 开始\n";
// 方式1:用普通函数创建线程
std::thread t1(print_message, "Hello, std::thread!");
// 方式2:用 lambda 表达式创建线程(最灵活)
std::thread t2([]() {
std::cout << "[线程] Lambda 执行\n";
});
// 方式3:用函数对象创建线程
Counter counter;
std::thread t3(counter, 3); // 传入函数对象和参数
// 注意:线程创建后立即执行,需用 join() 等待(后续详解)
t1.join();
t2.join();
t3.join();
std::cout << "[主线程] 结束\n";
return 0;
}
二、线程生命周期管理:join () 与 detach ()
2.1 核心规则
std::thread 对象析构前,必须明确指定线程的 "归宿" :要么用 join() 等待线程执行完毕,要么用 detach() 让线程在后台独立运行。若析构时对象仍处于 joinable() 状态(未 join 且未 detach),程序会直接调用 std::terminate() 崩溃 ------ 这是 std::thread 最容易踩的陷阱。
2.2 关键函数与用法
cpp
#include <iostream>
#include <thread>
#include <chrono> // 用于模拟耗时操作
void long_task() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟2秒耗时
std::cout << "[线程] 耗时任务完成\n";
}
int main() {
// 1. join():等待线程执行完毕,回收线程资源
{
std::thread t(long_task);
std::cout << "[主线程] 等待线程完成...\n";
t.join(); // 阻塞主线程,直到 t 执行完毕
std::cout << "[主线程] 线程已 join,资源回收\n";
// t 析构时已不 joinable,安全
}
// 2. detach():分离线程,线程在后台独立运行,不再受 thread 对象控制
{
std::thread t(long_task);
std::cout << "[主线程] 分离线程,让其后台运行\n";
t.detach(); // 分离后,t 不再 joinable
// 注意:detach 后线程可能访问已销毁的变量(如局部变量),需谨慎
}
// 3. joinable():判断线程是否可 join(未 join 且未 detach)
std::thread t(long_task);
std::cout << "[主线程] t 是否 joinable? " << (t.joinable() ? "是" : "否") << "\n";
t.join();
std::cout << "[主线程] join 后 t 是否 joinable? " << (t.joinable() ? "是" : "否") << "\n";
// 陷阱演示:未 join/detach 直接析构(会崩溃!)
// {
// std::thread t(long_task);
// } // t 析构时仍 joinable,程序 terminate
return 0;
}
三、线程参数传递:细节与陷阱
std::thread 的参数默认拷贝到线程栈(值传递),若需传递引用或不可拷贝对象,需特殊处理 ------ 这是最容易出错的地方。
3.1 完整用法示例
cpp
#include <iostream>
#include <thread>
#include <string>
#include <memory> // 用于 unique_ptr
// 1. 值传递:默认行为,安全但有拷贝开销
void func_by_value(int x) {
x += 10;
std::cout << "[线程] 值传递,x = " << x << "\n";
}
// 2. 引用传递:需用 std::ref()/std::cref(),否则会被当作值拷贝
void func_by_ref(int& x) {
x += 10;
std::cout << "[线程] 引用传递,x = " << x << "\n";
}
// 3. 移动语义:传递不可拷贝对象(如 unique_ptr),需用 std::move()
void func_by_move(std::unique_ptr<int> ptr) {
std::cout << "[线程] 移动传递,*ptr = " << *ptr << "\n";
}
// 4. 成员函数作为线程入口:需传入对象指针/引用 + 成员函数地址
class Worker {
public:
void do_work(const std::string& task) {
std::cout << "[线程] 成员函数执行任务: " << task << "\n";
}
};
int main() {
int a = 10;
std::cout << "[主线程] 初始 a = " << a << "\n";
// 方式1:值传递(默认)
std::thread t1(func_by_value, a);
t1.join();
std::cout << "[主线程] 值传递后 a = " << a << "\n"; // a 仍为 10(未修改)
// 方式2:引用传递(必须用 std::ref()!)
std::thread t2(func_by_ref, std::ref(a));
t2.join();
std::cout << "[主线程] 引用传递后 a = " << a << "\n"; // a 变为 20(已修改)
// 错误示例:不用 std::ref(),会编译报错(或被当作值拷贝,取决于编译器)
// std::thread t_error(func_by_ref, a);
// 方式3:移动语义(传递不可拷贝对象)
auto ptr = std::make_unique<int>(100);
std::thread t3(func_by_move, std::move(ptr));
t3.join();
// 注意:move 后 ptr 变为空,不能再访问
if (!ptr) std::cout << "[主线程] ptr 已失去所有权\n";
// 方式4:成员函数作为线程入口
Worker worker;
std::thread t4(&Worker::do_work, &worker, "处理数据"); // 传入成员函数地址、对象指针、参数
t4.join();
return 0;
}
四、std::thread 的移动语义与禁止拷贝
4.1 核心特性
std::thread 禁止拷贝 (删除了拷贝构造和拷贝赋值运算符),但支持移动语义 ------ 可以通过 std::move() 转移线程所有权,原对象不再 joinable。这使得 std::thread 可以存入容器(如 std::vector),但必须用移动操作。
4.2 用法示例
cpp
#include <iostream>
#include <thread>
#include <vector>
void task(int id) {
std::cout << "[线程 " << id << "] 执行\n";
}
int main() {
// 1. 移动构造:转移线程所有权
std::thread t1(task, 1);
std::thread t2 = std::move(t1); // t1 的所有权转移给 t2,t1 不再 joinable
if (!t1.joinable()) std::cout << "[主线程] t1 已失去所有权\n";
t2.join();
// 2. 用容器存储 thread(必须用移动)
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(task, i + 1); // 直接在容器中构造 thread
// 或:threads.push_back(std::thread(task, i + 1)); // 移动构造
}
// 遍历 join 所有线程
for (auto& t : threads) {
if (t.joinable()) t.join();
}
return 0;
}
五、线程同步基础:配合 mutex 解决数据竞争
std::thread 本身不处理同步,多线程共享数据时会出现数据竞争 (Data Race),导致未定义行为。需配合 <mutex> 库的互斥锁(std::mutex)和 RAII 包装器(std::lock_guard、std::unique_lock)解决。
5.1 完整示例:数据竞争与解决
cpp
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁头文件
#include <vector>
// 未同步的计数器:会出现数据竞争
class UnsafeCounter {
public:
int count = 0;
void increment() { count++; } // 非原子操作,多线程下会出错
};
// 同步的计数器:用 mutex 保护共享数据
class SafeCounter {
public:
int count = 0;
std::mutex mtx; // 互斥锁
void increment() {
// 用 std::lock_guard 自动管理锁:构造时 lock,析构时 unlock(RAII)
std::lock_guard<std::mutex> lock(mtx);
count++; // 临界区:同一时间只有一个线程能执行
}
};
void test_unsafe() {
UnsafeCounter counter;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&counter]() {
for (int j = 0; j < 1000; ++j) {
counter.increment();
}
});
}
for (auto& t : threads) t.join();
std::cout << "[未同步] 最终 count = " << counter.count << "(期望 10000,实际可能更小)\n";
}
void test_safe() {
SafeCounter counter;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&counter]() {
for (int j = 0; j < 1000; ++j) {
counter.increment();
}
});
}
for (auto& t : threads) t.join();
std::cout << "[同步] 最终 count = " << counter.count << "(期望 10000,实际一致)\n";
}
int main() {
test_unsafe();
test_safe();
return 0;
}
六、常见陷阱与最佳实践
6.1 核心陷阱
- 忘记 join/detach :
std::thread析构时若仍joinable,程序直接崩溃 ------ 务必在析构前明确处理。 - 引用传递不用
std::ref():参数会被当作值拷贝,导致修改不生效或编译报错。 - 分离线程访问已销毁变量 :
detach()后线程若访问局部变量(已随作用域结束销毁),会触发未定义行为。 - 未保护共享数据 :多线程共享数据时必须用同步机制(
mutex、原子变量等),否则会出现数据竞争。
6.2 最佳实践
优先用 join() :除非必须让线程后台运行,否则优先用 join() 明确等待线程结束,避免生命周期问题。
用 RAII 管理锁 :避免手动 lock()/unlock(),优先用 std::lock_guard(简单场景)或 std::unique_lock(灵活场景)。
避免共享数据:尽量让线程拥有独立数据,减少同步需求;若必须共享,明确用同步机制保护。
用 std::jthread(C++20) :C++20 引入的 std::jthread 自动在析构时 join(),更安全,若支持 C++20 优先使用。