Wiki
std::thread
std::thread 是 C++11 提供的标准线程管理类,定义在 <thread> 中。
这一节不只要学会“开一个线程”,更重要的是理解:
std::thread对象和实际执行线程是什么关系;- 构造函数里第一个参数和后续参数分别表示什么;
- 参数默认是如何保存的;
join()、detach()、joinable()分别改变了什么;- 为什么线程对象的析构函数可能直接终止程序;
- 为什么
std::thread不能复制,但可以移动。
1.std::thread 对象不是“线程本身”
可以把 std::thread 理解成一个 管理线程执行实体的 C++ 对象。
一个 std::thread 对象可能处于两种重要状态:
未关联任何执行线程
或
关联着一个可 join 的执行线程
这个状态可以通过:
thread.joinable()
查询。
例如:
#include <iostream>
#include <thread>
void work()
{
std::cout << "Work Done!" << std::endl;
}
int main()
{
std::thread t1;
std::cout << std::boolalpha << t1.joinable() << '\n';
std::thread t2(work);
std::cout << t2.joinable() << '\n';
t2.join();
std::cout << t2.joinable() << '\n';
}
输出:
false
true
Work Done!
false
注意:joinable() 并不是“线程现在是不是还在运行”。
即使线程函数已经执行完,只要还没有对对应的 std::thread 调用 join() 或 detach(),它仍然可能:
thread.joinable() == true
比如:
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
void work()
{
std::cout << "Work Done!" << std::endl;
}
int main()
{
std::thread t1;
std::cout << std::boolalpha << t1.joinable() << '\n';
std::thread t2(work);
std::cout << t2.joinable() << '\n';
std::this_thread::sleep_for(100ms);
std::cout << t2.joinable() << '\n';
t2.join();
std::cout << t2.joinable() << '\n';
}
结果是
false
true
Work Done!
true
false
这里明显任务早就结束了,但延时100ms后去看joinable()发现还是true。
所以joinable()看的是他现在能不能被join()或者detach(),上面两个例子中:
当这个线程对象使用默认构造,或者说没有被给一个可调用对象的时候,这时候是false;
还有这个线程对象已经被join()或者detach()的时候,也会变成false。
2.构造函数:第一个参数是什么?后面的参数是什么?
最常见的构造形式可以概念化为:
std::thread t(callable, arg1, arg2, ...);
其中:
- 第一个参数:要在线程中执行的可调用对象;
- 后续参数:传给这个可调用对象的实参。
可调用对象可以是:
- 普通函数;
- Lambda;
- 函数对象;
- 成员函数指针。
线程创建成功后,新线程会开始调用这个可调用对象。
2.1.普通函数
#include <iostream>
#include <thread>
void print_sum(int a, int b)
{
std::cout << a + b << '\n';
}
int main()
{
std::thread t(print_sum, 10, 20);
t.join();
}
这里:
print_sum
是第一个参数,即线程函数;
10, 20
是传给 print_sum 的参数。
2.2.Lambda
#include <iostream>
#include <thread>
int main()
{
std::thread t([](int value) {
std::cout << value << '\n';
}, 42);
t.join();
}
Lambda 本身就是第一个可调用对象参数,42 是它的实参。
2.3.函数对象
#include <iostream>
#include <thread>
struct Worker
{
void operator()(int value) const
{
std::cout << value << '\n';
}
};
int main()
{
std::thread t(Worker{}, 7);
t.join();
}
2.4.成员函数
调用成员函数时,除了成员函数指针,还必须告诉线程“在哪个对象上调用”。
#include <iostream>
#include <thread>
class Worker
{
public:
void run(int value)
{
std::cout << value << '\n';
}
};
int main()
{
Worker worker;
std::thread t(&Worker::run, &worker, 100);
t.join();
}
这里:
&Worker::run
是成员函数指针;
&worker
表示在哪个对象上调用;
100
才是 run(int) 自己的参数。
3.线程参数默认会保存副本
这是 std::thread 最容易让初学者误解的地方之一。
例如:
void foo(int value);
int x = 10;
std::thread t(foo, x);
线程并不是简单地“记住表达式 x”。标准库会把传入参数经过衰减后保存进线程内部,再在新线程中调用。
因此很多情况下,线程拿到的是参数副本。
3.1.按值传递
#include <iostream>
#include <thread>
void change(int value)
{
value = 100;
}
int main()
{
int x = 10;
std::thread t(change, x);
t.join();
std::cout << x << '\n';
}
输出仍然是:
10
因为线程修改的是自己的参数副本。
4.std::ref():真正按引用传递
如果线程函数要求:
void change(int& value);
并希望修改原对象,应使用:
std::ref(x)
例如:
#include <functional>
#include <iostream>
#include <thread>
void change(int& value)
{
value = 100;
}
int main()
{
int x = 10;
std::thread t(change, std::ref(x));
t.join();
std::cout << x << '\n';
}
输出:
100
std::ref(x) 返回的是一个:
std::reference_wrapper<T>
它本身可以被复制,但调用可调用对象时会还原为对原对象的引用。
4.1.std::cref()
如果需要按 const T& 传递,可以使用:
std::cref(x)
例如:
void print(const std::string& text);
std::string message = "hello";
std::thread t(print, std::cref(message));
5.std::ref(x) 和 &x 完全不是一回事
这两个写法经常被混淆。
std::ref(x)
表示“以引用语义传递原对象”。
而:
&x
是取地址,类型是指针。
例如:
void f1(int& value);
void f2(int* value);
int x = 10;
std::thread t1(f1, std::ref(x));
std::thread t2(f2, &x);
两者对应的是不同的形参类型。
6.Lambda 捕获也要注意值和引用
线程和 Lambda 经常配合使用。
按值捕获:
int x = 10;
std::thread t([x] {
std::cout << x << '\n';
});
Lambda 内保存自己的 x 副本。
按引用捕获:
int x = 10;
std::thread t([&x] {
x = 20;
});
t.join();
线程直接访问外部的 x。
按引用捕获不会延长 x 的生命周期,因此必须确保线程使用 x 时,x 仍然存在。
7.join():等待线程结束
t.join();
表示:
当前线程在这里阻塞,直到
t所关联的线程执行结束。
例如:
#include <chrono>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
void work()
{
std::this_thread::sleep_for(1s);
std::cout << "worker done\n";
}
int main()
{
std::thread t(work);
std::cout << "before join\n";
t.join();
std::cout << "after join\n";
}
执行关系是:
创建 t
↓
work 与 main 并发执行
↓
main 到达 join
↓
等待 work 结束
↓
join 返回
↓
main 继续执行
调用成功后:
t.joinable() == false
7.1.join() 不会自动延长任意对象生命周期
join() 只是在它被调用的位置等待目标线程。
下面是安全的:
int x = 10;
std::thread t([&x] {
++x;
});
t.join();
// 到这里线程已经结束,而 x 还活着
但如果对象先离开作用域,再去等待线程,就已经太迟:
std::thread t;
{
int x = 10;
t = std::thread([&x] {
// 可能访问 x
});
} // x 在这里销毁
// 此时线程可能仍在访问已经销毁的 x
t.join();
8.detach():线程脱离管理
t.detach();
官方定义:调用 detach() 后,线程会继续独立运行,而当前 std::thread 对象不再与该线程关联。
为了方便理解,也可以“民办”地理解成:
detach() 一调用,当前线程对象就会立刻变成不可 joinable();
它有点像一个“不会阻塞的 join()”——区别在于,join() 会等待线程执行结束后再返回,而 detach() 不会等待,调用后立即返回,让线程自己在后台继续运行。
因此:
t.joinable() == false
detach() 最大的问题是生命周期很难管理。
错误示例:
void start()
{
int value = 42;
std::thread t([&value] {
std::cout << value << '\n';
});
t.detach();
} // value 已经销毁,但脱离线程可能仍在使用它
这会产生悬空引用和未定义行为。
初学和普通工程代码中,优先考虑明确 join(),不要为了“让线程后台跑”就习惯性 detach()。
9.joinable():判断能否 join
if (t.joinable())
{
t.join();
}
常见状态变化:
| 操作 | joinable() |
|---|---|
默认构造 std::thread t; |
false |
std::thread t(f); |
true |
| 线程函数已经执行完,但尚未 join/detach | true |
调用 join() 后 |
false |
调用 detach() 后 |
false |
| 被 move-from 的 thread | false |
10.为什么忘记 join/detach 会直接终止程序?
如果一个 std::thread 对象析构时仍满足:
t.joinable() == true
程序会调用:
std::terminate()
这个std::terminate()会立刻终止整个程序。它不是“结束当前线程”,而是整个进程直接进入异常终止流程。
例如:
#include <thread>
void work()
{
}
int main()
{
std::thread t(work);
} // 错误:t 析构时仍然 joinable
可以把规则概念化成:
~thread()
{
if (joinable())
{
std::terminate();
}
}
这不是标准库源码,只是帮助理解析构语义。
标准库不会偷偷帮你自动 join(),因为自动等待可能让程序在一个意料之外的位置长时间阻塞;也不会偷偷 detach(),因为那可能制造更严重的生命周期错误。
11.std::thread 不可复制,但可以移动
线程的管理权不能被两个 std::thread 对象同时拥有,因此它不可复制:
std::thread t1(work);
// std::thread t2 = t1; // 编译错误
但可以转移所有权:
std::thread t1(work);
std::thread t2 = std::move(t1);
移动后:
t1.joinable() == false
t2.joinable() == true
最后应该由 t2 负责 join() 或 detach()。
这也是为什么 std::vector<std::thread> 可以工作:容器通过移动而不是复制管理线程对象。
12.std::this_thread
std::this_thread 提供“操作当前线程”的工具。但他不是只能在std::thread的对象内使用。
12.1.获取线程 ID
std::this_thread::get_id()
示例:
#include <iostream>
#include <thread>
int main()
{
std::cout << std::this_thread::get_id() << '\n';
}
12.2.休眠
std::this_thread::sleep_for(...)
std::this_thread::sleep_until(...)
| 函数 | 参数 | 含义 |
|---|---|---|
sleep_for(duration) |
时间长度 | 再睡多久 |
sleep_until(time_point) |
时间点 | 睡到什么时候 |
例如:
//睡100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));
//到相对于now()之后的100ms
auto next = std::chrono::steady_clock::now();
next += 100ms;
std::this_thread::sleep_until(next);
12.3.主动让出执行机会
std::this_thread::yield();
它只是向调度器提示“当前线程愿意暂时让出执行机会”,并不保证另一个指定线程一定马上运行,也不保证下一个运行的线程不是自己。
比较形象一点的说法:当前线程主动告诉操作系统调度器:“我现在可以先让一下 CPU,你可以考虑让别的线程先运行。”
yield() 只是“主动让出这次 CPU 调度机会”,但马上又被调度回来也是完全可能的。
比如:
while (!ready)
{
std::this_thread::yield();
}
这种比while (!ready) {}这种空转要好很多,因为空转会一直霸占 CPU,而 yield() 会给其他线程更多运行机会。
13.hardware_concurrency()
std::thread::hardware_concurrency()
一般用来查询:当前系统大概支持多少个线程可以并行执行。一般是CPU 的逻辑核心数 / 硬件线程数。
#include <iostream>
#include <thread>
int main()
{
std::cout << "建议并发线程数:" << std::thread::hardware_concurrency() << '\n';
}
假设你的 CPU 是 8 核 16 线程,那么 std::thread::hardware_concurrency() 通常会返回 16。
但这并不意味着程序最多只能创建 16 个线程。它表示的是当前硬件大约能够支持多少个线程真正并行执行,也就是硬件并发能力的参考值。 即使只有 16 个逻辑处理器,只要系统资源允许,依然可以创建几十个甚至上百个线程。
当线程数量超过硬件能够真正并行执行的数量时,操作系统会通过时间片轮转、线程调度等机制,让这些线程交替获得 CPU 执行时间,从宏观上看起来像是在“同时运行”。虽然这不是真正意义上的完全并行,但在很多场景下,从程序运行效果上看和真正并发并没有特别明显的区别。(类似STM32上使用的FreeRTOS,用1个CPU内核来模拟多线程)
另外,如果函数返回 0,表示当前实现无法提供硬件并发信息,因此不能把这个返回值当作绝对可靠的固定值。
14.多线程输出顺序是不确定的
#include <iostream>
#include <thread>
void print(char c)
{
for (int i = 0; i < 5; ++i)
{
std::cout << c;
}
}
int main()
{
std::thread t1(print, 'A');
std::thread t2(print, 'B');
t1.join();
t2.join();
}
不能依赖输出一定是:
AAAAA
BBBBB
完全有可能是
BBBBB
AAAAA
等很多可能性。

线程调度顺序由操作系统和运行环境决定。如果执行顺序本身是程序正确性的必要条件,就必须显式同步。
15.常见错误
15.1.错误 1:忘记 join 或 detach
std::thread t(work);
// 离开作用域前没有处理 t
结果可能是 std::terminate()。
15.2.错误 2:把引用形参误认为 thread 会自动按引用传递
void foo(int& x);
int value = 10;
// std::thread t(foo, value); // 不要把它理解成自动引用传递
需要原对象引用语义时写:
std::thread t(foo, std::ref(value));
15.3.错误 3:detach 后引用局部变量
局部变量可能在线程使用前就销毁。
15.4.错误 4:重复 join
t.join();
// t.join(); // 此时已经不再 joinable
15.5.错误 5:认为线程函数结束后 joinable 自动变 false
线程执行完和线程对象解除关联是两件不同的事情。
16.C++20:优先了解 std::jthread
std::thread 的一个明显风险是:必须手动确保所有控制路径最终都正确 join() 或 detach()。
C++20 提供了:
std::jthread
它在析构时会自动请求停止并等待线程结束,而且支持 std::stop_token 协作式取消。
基础原理理解完 std::thread 后,建议继续学习本章后面的 std::jthread 与 std::stop_token。
17.小结
std::thread t(f, args...)的第一个参数是可调用对象,后面的参数是传给它的实参。- 线程参数默认会被保存,引用语义通常要显式使用
std::ref()/std::cref()。 join()等待线程结束,并让线程对象变为 non-joinable。detach()让线程脱离当前std::thread对象,生命周期风险较大。joinable()判断对象是否仍关联一个可 join 的线程,不等于线程此刻是否正在运行。- joinable 的
std::thread析构会调用std::terminate()。 std::thread不可复制,但可以移动。- 线程共享引用、指针或对象时,必须额外考虑对象生命周期和数据竞争。