一、C++17 中 std::thread 的详细使用方法
std::thread 是 C++11 引入并在 C++17 中进一步完善的多线程核心类,位于 <thread> 头文件中。它提供了一种面向对象的方式来创建和管理操作系统级别的线程。
1. 基本构造与启动线程
(1) 构造函数语法
#include <thread>
// 使用函数
std::thread t1(worker_function);
// 使用带参数的函数
std::thread t2(worker_function, arg1, arg2);
// 使用 lambda 表达式
std::thread t3([](int x) {
std::cout << "Lambda thread: " << x << std::endl;
}, 42);
// 使用函数对象(仿函数)
struct Worker {
void operator()() const {
std::cout << "Functor thread running\n";
}
};
std::thread t4(Worker{});
注意:参数是按值传递的。如果需要引用,必须使用
std::ref()包装。
示例:传递引用参数
void increment(int& value) {
++value;
}
int main() {
int counter = 0;
std::thread t(increment, std::ref(counter)); // 必须用 std::ref
t.join();
std::cout << counter << std::endl; // 输出 1
return 0;
}
2. 等待线程结束:join() 和 detach()
每个 std::thread 对象在销毁前必须调用 join() 或 detach(),否则程序会调用 std::terminate() 终止。
| 方法 | 作用 | 使用场景 |
|---|---|---|
t.join() | 阻塞当前线程,直到 t 执行完毕 | 等待结果或资源清理 |
t.detach() | 将线程分离,独立运行,不再可被 join | 后台任务,不关心生命周期 |
示例:join vs detach
void background_task() {
for (int i = 0; i < 5; ++i) {
std::cout << "Background task: " << i << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread t(background_task);
// 主线程做其他事...
std::this_thread::sleep_for(std::chrono::milliseconds(300));
// 方式1:等待线程结束
if (t.joinable()) {
t.join(); // 确保只 join 一次
}
// 方式2:分离线程(注释掉上面的 join)
// t.detach();
return 0;
}
3. 检查线程状态:joinable()
if (t.joinable()) {
t.join(); // 安全地 join
}
joinable()返回true当线程正在运行且未被join或detach。- 调用
join()或detach()后,joinable()返回false。
4. 移动语义支持
std::thread 支持移动,但不支持拷贝:
std::thread t1([]{ std::cout << "Hello\n"; });
// std::thread t2 = t1; // 错误!不能拷贝
std::thread t3 = std::move(t1); // 正确:移动
if (t1.joinable()) {
std::cout << "t1 is no longer a thread\n"; // t1 现在为空
}
t3.join();
可用于将线程放入容器:
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back([i] {
std::cout << "Thread " << i << " running\n";
});
}
// 统一 join
for (auto& t : threads) {
if (t.joinable()) t.join();
}
5. 获取线程 ID
std::thread::id get_id() const noexcept;
// 示例
void print_id() {
std::cout << "Thread ID: " << std::this_thread::get_id() << std::endl;
}
int main() {
std::thread t(print_id);
std::cout << "Main thread ID: " << t.get_id() << std::endl;
t.join();
return 0;
}
6. 线程休眠控制
使用 <chrono> 提供的时间工具:
#include <chrono>
#include <thread>
std::this_thread::sleep_for(std::chrono::seconds(1));
std::this_thread::sleep_until(std::chrono::system_clock::now() +
std::chrono::milliseconds(500));
二、C++17 中使用 std::thread 的注意事项
1. 必须调用 join() 或 detach()
未调用会导致 std::terminate():
{
std::thread t(some_work);
} // 析构时未 join/detach → terminate!
✅ 正确做法:
std::thread t(some_work);
// ... 其他操作
t.join(); // 或 t.detach();
建议优先使用 join(),除非明确需要后台运行。
2. 避免数据竞争(Data Race)
多个线程访问共享数据时必须同步:
❌ 危险示例:
int global_counter = 0;
void unsafe_increment() {
for (int i = 0; i < 1000; ++i) ++global_counter; // 数据竞争!
}
✅ 正确做法:
std::mutex mtx;
void safe_increment() {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++global_counter;
}
}
3. 参数传递陷阱:右值引用会被移动
void process_string(std::string& s) { /* 修改 s */ }
std::string data = "hello";
std::thread t(process_string, data); // 错误!data 被复制,无法修改原变量
✅ 正确方式:
std::thread t(process_string, std::ref(data)); // 使用 std::ref
4. 异常安全问题
如果线程函数抛出异常,而主线程未捕获,可能导致资源泄漏。
void may_throw() {
throw std::runtime_error("Oops!");
}
int main() {
std::thread t(may_throw);
try {
t.join(); // 异常会传播到此处
} catch (...) {
std::cout << "Exception caught\n";
t.join(); // ❌ 错误!已经 join 过了
}
}
✅ 正确做法:
try {
t.join();
} catch (...) {
if (t.joinable()) t.join(); // 不可能,因为 join 抛异常后 t 已不可 join
// 更好的方式:在子线程内部处理异常
}
推荐:在线程内部捕获所有异常。
5. 不要返回局部变量的指针或引用
std::thread& create_thread() {
std::thread t([]{});
return t; // 返回局部对象引用 → 悬空引用
}
6. 线程与对象生命周期管理
确保线程使用的资源在其生命周期内有效:
void worker(const std::string& s) {
std::cout << s << std::endl;
}
{
std::string temp = "temporary";
std::thread t(worker, std::cref(temp));
t.detach(); // 分离后,temp 可能已被销毁 → 未定义行为
} // temp 析构
✅ 解决方案:使用 join() 或确保资源生命周期足够长。
三、C++ 线程的实现原理
1. std::thread 的底层机制
std::thread 是对操作系统原生线程 API 的封装:
| 平台 | 底层实现 |
|---|---|
| Linux | POSIX Threads (pthread_create, pthread_join) |
| Windows | Windows Thread API (CreateThread, WaitForSingleObject) |
| macOS | pthreads(基于 Mach 微内核) |
当创建 std::thread 时:
- 调用系统 API 创建新线程。
- 新线程执行用户提供的可调用对象(函数、lambda 等)。
std::thread对象持有线程句柄(handle)和状态信息。
2. 线程模型:1:1 模型
C++ 标准库采用 1:1 线程模型:
- 每个
std::thread对应一个操作系统内核线程。 - 优点:并发真正并行,适合 CPU 密集型任务。
- 缺点:线程创建开销大,数量受限于系统资源。
对比:N:M 模型(绿色线程)由用户态调度,开销小但难以充分利用多核。
3. 线程调度
- 由操作系统内核调度器决定哪个线程在哪个 CPU 核心上运行。
- C++ 标准不提供优先级设置接口(需平台特定 API,如
pthread_setschedparam)。 - 可通过
std::this_thread::yield()提示调度器让出 CPU。
4. 内存模型与线程通信
C++11 引入了 内存模型(Memory Model),定义了多线程环境下变量的可见性和顺序。
关键概念:
- Happens-before: 定义操作顺序关系。
- Synchronizes-with: 如锁的释放与获取建立同步关系。
- 原子操作:保证读写不可分割,避免数据竞争。
例如:
std::mutex mtx;
int data = 0;
bool ready = false;
// 线程1
void producer() {
data = 42;
std::lock_guard<std::mutex> lk(mtx);
ready = true; // 释放锁
}
// 线程2
void consumer() {
std::lock_guard<std::mutex> lk(mtx);
if (ready) { // 获取锁
std::cout << data; // 一定看到 data == 42
}
}
锁的“释放-获取”建立了 synchronizes-with 关系,确保 data 的写入对读取可见。
5. 线程局部存储(TLS)
使用 thread_local 关键字声明每个线程独有的变量:
thread_local int thread_id = 0;
void set_id(int id) {
thread_id = id;
std::cout << "My ID: " << thread_id << std::endl;
}
每个线程有独立副本,避免共享和同步。
四、总结
| 项目 | 说明 |
|---|---|
| 头文件 | <thread> |
| 构造方式 | 函数、lambda、函数对象,支持参数传递 |
| 生命周期管理 | 必须 join() 或 detach() |
| 移动语义 | 支持移动,不支持拷贝 |
| 同步机制 | 配合 mutex、atomic、condition_variable 使用 |
| 底层实现 | 封装操作系统原生线程(1:1 模型) |
| 内存模型 | 基于 C++ 内存模型,保证顺序和可见性 |
最佳实践建议:
- 优先使用
join()而非detach()。 - 使用 RAII 管理锁(
std::lock_guard、std::unique_lock)。 - 避免共享可变状态,优先使用消息传递或无锁结构。
- 在线程内部处理异常。
- 注意参数传递语义(值 vs 引用)。
- 考虑使用更高层次的抽象(如
std::async、线程池)简化开发。
通过合理使用 std::thread 和配套同步机制,可以编写高效、安全的多线程 C++ 程序。
9156

被折叠的 条评论
为什么被折叠?



