1.std::thread
1.1.头文件与基本概念
c
#include <thread>
std::thread 是 C++11 引入的线程类,代表单个执行线程。每个 std::thread 对象要么:
- 关联一个实际操作系统线程(joinable)
- 不关联任何线程(non-joinable / not-a-thread)
1.2.构造函数
| 构造函数 | 说明 | 可用性 |
|---|---|---|
thread() noexcept |
默认构造,不关联任何线程 | ✓ |
thread(thread&& other) noexcept |
移动构造,转移线程所有权 | ✓ |
thread(const thread&) = delete |
拷贝构造被删除,线程不可拷贝 | ✗ |
template<class F, class... Args> thread(F&& f, Args&&... args) |
有参构造 ,启动新线程执行 f(args...) |
✓ |
1.3.赋值操作
| 操作 | 说明 |
|---|---|
thread& operator=(thread&& other) noexcept |
移动赋值,转移线程所有权 |
thread& operator=(const thread&) = delete |
拷贝赋值被删除 |
移动赋值规则
c
std::thread t1(func);
std::thread t2;
t2 = std::move(t1); // OK:t1 的所有权转移给 t2
// 注意:如果被赋值方(t2)当前是 joinable 的,调用 terminate()
⚠️ 关键陷阱:如果赋值目标当前是 joinable 的,程序会调用 std::terminate() 终止!
1.4.开启新线程的方式
1.4.1.普通函数
c
void task(int id) { /* ... */ }
std::thread t(task, 42);
1.4.2.Lambda 表达式(最常用)
c
std::thread t([]() {
std::cout << "Hello from thread\n";
});
1.4.3.仿函数(Function Object)
c
struct Task {
void operator()(int x) const { /* ... */ }
};
std::thread t(Task(), 100);
1.4.4.成员函数
c
class Worker {
public:
void doWork(int param) { /* ... */ }
};
Worker w;
std::thread t(&Worker::doWork, &w, 42); // 第1个参数是成员函数指针,第2个是对象指针/引用
1.4.5.bind 绑定
c
using namespace std::placeholders;
std::thread t(std::bind(func, 10, _1), 20);
1.5.通过传参开启新线程及内部原理
1.5.1.基本用法
c
void process(int n, const std::string& str, double* ptr);
std::thread t(process, 100, "hello", &value);
1.5.2.内部原理:Decay 拷贝 + 完美转发
c
template<class F, class... Args>
thread(F&& f, Args&&... args) {
// 伪代码:
// 1. 所有参数进行 decay-copy(值拷贝)
// 2. 在新线程内部,用完美转发调用 f(decay_copy(args)...)
}
核心机制:
| 机制 | 说明 |
|---|---|
| Decay-copy | 参数会先被拷贝到线程内部存储,即使原参数是引用。这是为了防止悬垂引用(dangling reference) |
| 完美转发 | 在新线程启动时,用 std::forward 将存储的参数传递给目标函数 |
| 类型退化 (decay) | 数组退化为指针,函数退化为函数指针,const/volatile 和引用被移除 |
1.5.3.传参陷阱与解决方案
1.陷阱 1:引用参数会被拷贝
c
void update(int& x) { x = 42; }
int value = 0;
std::thread t(update, value); // ❌ 编译错误!value 被 decay 为 int,不是 int&
解决方案:使用 std::ref / std::cref
c
std::thread t(update, std::ref(value)); // ✓ 显式传递引用
t.join();
// value 现在是 42
2.陷阱 2:指针/引用指向局部变量
c
void bad() {
int local = 10;
std::thread t([](int* p) { *p = 20; }, &local);
t.detach(); // ❌ 灾难!线程可能在函数返回后才执行,local 已销毁
}
陷阱 3:字符串字面量被 decay 为 const char*
c
void func(std::string s);
std::thread t(func, "hello"); // OK:const char* 可以构造 string
// 但如果函数参数是 const char*,而传入 string 对象,可能不匹配
陷阱 4:类成员函数传参
c
class Foo {
public:
void bar(int x, std::string s);
};
Foo foo;
std::thread t(&Foo::bar, &foo, 42, "hello");
// 参数分解:(&foo)->bar(42, "hello")
1.6.线程所有权转移 (Move Semantics)
std::thread 是资源句柄(类似 std::unique_ptr),其所有权可以移动但不能拷贝。
1.6.1.所有权转移场景
c
std::thread createThread() {
std::thread t([]() {
std::cout << "In new thread\n";
});
return t; // ✓ 隐式移动(NRVO / 移动语义)
}
int main() {
std::thread t = createThread(); // 所有权从函数转移到 t
t.join();
}
1.6.2.存入容器
c
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back([i]() { // ✓ emplace_back 直接构造,避免拷贝
std::cout << "Worker " << i << "\n";
});
}
for (auto& t : workers) {
if (t.joinable()) t.join();
}
1.6.3.RAII 包装(最佳实践)
由于 std::thread 析构时如果是 joinable 会调用 terminate(),通常需要 RAII 包装:
c
class ThreadGuard {
std::thread& t;
public:
explicit ThreadGuard(std::thread& _t) : t(_t) {}
~ThreadGuard() {
if (t.joinable()) {
t.join(); // 或 t.detach(),视需求而定
}
}
ThreadGuard(const ThreadGuard&) = delete;
ThreadGuard& operator=(const ThreadGuard&) = delete;
};
// 使用
std::thread t(func);
ThreadGuard guard(t); // 保证异常安全
1.7.重要成员函数
| 函数 | 说明 |
|---|---|
join() |
阻塞等待线程完成 |
detach() |
分离线程,不再关联,线程独立运行 |
joinable() |
检查是否关联活跃线程 |
get_id() |
返回线程 ID |
native_handle() |
返回底层 OS 线程句柄(平台相关) |
swap(thread&) |
交换两个 thread 对象 |
hardware_concurrency() 静态 |
返回支持的并发线程数 |
1.8.完整综合示例
c
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
void worker(int id, int& result, std::mutex& mtx) {
int local = id * 10;
std::lock_guard<std::mutex> lock(mtx);
result += local;
std::cout << "Worker " << id << " done\n";
}
int main() {
int total = 0;
std::mutex mtx;
std::vector<std::thread> threads;
// 创建多个线程,传递引用参数
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i, std::ref(total), std::ref(mtx));
}
// 所有权在 vector 中,统一 join
for (auto& t : threads) {
if (t.joinable()) t.join();
}
std::cout << "Total: " << total << "\n"; // Total: 60
return 0;
}
1.9.关键要点总结
- 不可拷贝:std::thread 只能移动,不能拷贝
- 参数 decay:默认所有参数按值拷贝到线程内部,引用需用 std::ref
- 所有权管理:移动语义支持容器存储和函数返回
- join/detach 二选一:析构前必须调用其一,否则 terminate()
1.10.decay与传参专项
很多人误以为 std::thread(func, arg1, arg2) 只是简单地把参数完美转发给 func。实际上,标准规定了一个两步机制:
c
主线程 新线程
─────────────────────────────────────────────────────────
thread(func, arg1, arg2)
│
▼
┌─────────────────┐
│ Step 1: 参数被 │ 这些副本存储在线程
│ "decay-copy" │ 对象内部的存储空间中
│ 创建副本 │
└─────────────────┘
│
▼
[ 新线程启动 ] ──────────► ┌─────────────────┐
│ Step 2: 用这些 │
│ 副本调用 func │
│ (完美转发) │
└─────────────────┘
1.10.1.什么是 Decay-Copy?
标准中的伪代码逻辑:
c
template<class F, class... Args>
thread(F&& f, Args&&... args) {
// 1. 对每个参数做 decay-copy,存储在线程内部
auto stored_args = std::make_tuple(
decay_copy(std::forward<Args>(args))...
);
// 2. 新线程启动时,用存储的副本调用 f
// invoke(f, get<0>(stored_args), get<1>(stored_args), ...)
}
其中 decay_copy(x) 等价于:
c
typename std::decay<decltype(x)>::type tmp(x); // 按值拷贝构造
std::decay 会做什么?
| 原始类型 | Decay 后类型 | 影响 |
|---|---|---|
int& |
int |
引用被移除! |
const int& |
int |
const 和引用都被移除 |
int[5] |
int* |
数组退化为指针! |
void(int) |
void(*)(int) |
函数退化为函数指针 |
volatile int |
int |
volatile 被移除 |
1.10.2.为什么必须 Decay-Copy?------ 生命周期安全
反例:如果直接传引用
c
void update(int& x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
x = 42; // 写入引用
}
void danger() {
int local = 0;
std::thread t(update, local); // 假设这里直接传引用
t.detach();
// danger() 返回,local 被销毁!
// 但新线程还在 sleep,醒来后发现 local 已经不存在了
// → 悬垂引用 (Dangling Reference) → 未定义行为 (UB)
}
C++ 标准委员会的设计决策:默认必须安全。如果用户真的需要引用,必须显式表达这个意图(用 std::ref),并自行承担生命周期管理责任。
1.10.3.std::ref 的工作原理
1.10.3.1.std::reference_wrapper 的本质
std::ref(x) 返回一个 std::reference_wrapper,它不是引用,而是一个可拷贝的轻量对象:
c
// 简化版实现
template<class T>
class reference_wrapper {
T* ptr; // 注意:存的是指针,不是引用!
public:
reference_wrapper(T& t) : ptr(std::addressof(t)) {}
// 可以隐式转换回引用
operator T& () const { return *ptr; }
T& get() const { return *ptr; }
// 支持拷贝(因为 ptr 可以拷贝)
reference_wrapper(const reference_wrapper&) = default;
};
1.10.3.2.使用 std::ref 后的完整流程
c
void foo(int& x) { x = 100; }
int main() {
int a = 0;
// 发生了什么?
std::thread t(foo, std::ref(a));
// │ │
// │ └─► 创建 reference_wrapper<int>(&a)
// │ 这是一个对象,可以安全 decay-copy
// │
// └─► 新线程内部:reference_wrapper 隐式转为 int&
// 实际调用 foo(a) ------ 真正的引用!
t.join();
std::cout << a; // 100 ✓
}
关键洞察:
- std::ref(a) 产生的对象被值拷贝到线程内部(安全,因为它只是个小对象)
- 但这个小对象内部持有指向 a 的指针
- 在新线程中调用时,它把指针解引用,还原成对原变量的引用
1.10.4.代码对比实验
实验 1:不加 std::ref ------ 编译失败
c
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int value = 0;
std::thread t(increment, value); // ❌ 编译错误!
/*
错误信息大致是:
error: no matching constructor...
note: candidate expects different arguments...
原因:value (int&) 被 decay 成了 int,
但 increment 期望 int&,类型不匹配
*/
t.join();
}
实验 2:加 std::ref ------ 成功
c
#include <thread>
#include <functional> // for std::ref
void increment(int& x) {
++x;
}
int main() {
int value = 0;
std::thread t(increment, std::ref(value)); // ✓ 编译通过
t.join();
// value == 1
}
实验 3:数组的 Decay
c
#include <thread>
#include <iostream>
void print_array(int arr[5]) { // 实际上等价于 int* arr
std::cout << sizeof(arr); // 输出指针大小 (8),不是 20!
}
int main() {
int data[5] = {1,2,3,4,5};
std::thread t(print_array, data); // data 被 decay 为 int*
t.join();
}
实验 4:const 被移除
c
void modify(int x) { // 传值
x = 999; // 只修改副本
}
int main() {
const int value = 42;
std::thread t(modify, value); // value 被 decay 为 int,const 被移除
t.join();
// value 仍然是 42
}
1.10.5.总结表
| 你的代码 | 实际存储在线程内部 | 新线程中调用时 |
|---|---|---|
std::thread(f, int_var) |
int 的副本 |
传这个 int 副本 |
std::thread(f, std::ref(int_var)) |
reference_wrapper<int> 的副本(内含指针) |
解引用为 int& |
std::thread(f, std::cref(int_var)) |
reference_wrapper<const int> 的副本 |
解引用为 const int& |
std::thread(f, array) |
数组首地址指针 | 传指针 |
std::thread(f, lambda) |
lambda 的副本(或移动后的副本) | 传 lambda |
1.10.6.最佳实践
c
#include <thread>
#include <functional>
#include <vector>
// ✅ 需要修改原变量 → 用 std::ref
void update(std::vector<int>& data) {
data.push_back(42);
}
// ✅ 只读访问大对象 → 用 std::cref(避免拷贝)
void read_only(const std::vector<int>& data) {
// 读取 data...
}
// ✅ 线程安全的值传递 → 直接传
void process(int value) {
// 用 value 的副本工作
}
int main() {
std::vector<int> vec = {1, 2, 3};
int x = 10;
std::thread t1(update, std::ref(vec)); // 修改原 vector
std::thread t2(read_only, std::cref(vec)); // 只读引用,不拷贝
std::thread t3(process, x); // 安全地传值
t1.join(); t2.join(); t3.join();
}
一句话记忆:std::thread 默认"防呆"------所有参数先拷贝一份保命;如果你确定要引用,必须用 std::ref 或 std::cref 显式声明,并自己保证原对象生命周期足够长。
1.10.7.答疑
c
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int value = 0;
std::thread t(increment, value); // ❌ 编译错误!
t.join();
}
解释上述编译中不匹配报错,正常下传递int变量给接受int&作为形参的函数应该可以?
对比:普通调用 vs std::thread 调用
场景 A:普通函数调用(✅ 编译通过)
c
void increment(int& x) { ++x; }
int main() {
int value = 0;
increment(value); // ✅ OK
}
发生了什么:
c
increment(value)
│
▼
value 是具名变量(左值 lvalue)
│
▼
绑定到 int& x(非 const 左值引用)
│
▼
✅ 合法:左值引用可以绑定到左值
场景 B:std::thread 调用(❌ 编译失败)
c
std::thread t(increment, value);
发生了什么:
c
// std::thread 构造函数模板(简化)
template<class F, class... Args>
thread(F&& f, Args&&... args) {
// Step 1: 参数被 decay-copy
// value 的类型被推导为 int(不是 int&)
// 然后创建副本:int __tmp = value;
// Step 2: 在新线程内部调用
// increment(__tmp);
// │
// ▼
// __tmp 是一个临时副本(虽然它有名字,但在转发后表现为右值/可移动对象)
// 更准确地说,内部用 std::forward<int>(__tmp) 转发
// │
// ▼
// 试图绑定到 int& x(非 const 左值引用)
// │
// ▼
// ❌ 非法:非 const 左值引用不能绑定到右值/临时对象!
}
根本原因:std::thread 的"参数存储"层
std::thread 不是直接把你的参数递给函数,而是先拷贝一份存起来,等线程启动时再用这份副本去调用你的函数。
c
普通调用:
value ──────► increment(int& x)
(左值) 直接绑定,OK
std::thread 调用:
value ──► [thread 内部存储] ──► increment(int& x)
(左值) 创建了副本 tmp 试图用 tmp 绑定到 int&
(转发时成为右值) ❌ 非 const 左值引用不能接右值!
形参为const int&则可以,因为此时实参可以是右值/可移动对象。
为什么 std::ref 能解决问题?
c
std::thread t(increment, std::ref(value));
std::ref(value) 创建的是 std::reference_wrapper 对象:
- 这个对象本身被值拷贝到线程内部(安全,它只是个轻量包装器)
- 但它内部持有 value 的地址
- 调用时,reference_wrapper 隐式转换为 int&,相当于 increment(value) 的原始语义被还原
c
std::thread t(increment, std::ref(value));
内部存储:
reference_wrapper<int> tmp(std::ref(value));
// tmp 里存的是 &value
新线程调用时:
increment(tmp); // tmp 隐式转为 int&,即 value 本身
// 相当于 increment(value) ------ 完美还原!