跳转到内容
新建笔记

C++ 线程:启动参数、join 与对象寿命

线程是进程内的一条执行路径。一个进程可以有多个线程;这些线程共享进程中的地址空间和许多资源,同时各自具有执行上下文和调用栈。多进程也可以是“每个进程都包含多个线程”,不能把二者当作互斥选择。

本页以 **C++17 的 std::thread**为主,重点是启动参数、对象寿命和结束时谁负责等待。同步方法另见互斥量、条件变量和线程池。

C++并发库导图:thread和this_thread、互斥量及锁管理、原子、条件变量和future

图是接口导航,不代表这些机制可以相互替代。例如线程负责执行,互斥量保护共享状态,future传递异步结果;仅仅“创建了线程”并没有完成同步。

维度多线程多进程
内存关系通常共享进程内对象,可直接传递有效地址,但必须处理同步和寿命。通常有独立地址空间,需IPC或显式共享内存。
故障边界某个线程造成的内存破坏可影响整个进程。操作系统通常提供更强隔离,但共享资源故障仍可传播。
开销创建、调度、栈和锁竞争都有代价。创建、地址空间及通信通常更重,实际结果取决于系统和任务。
适用例子同一应用内的并行计算、后台I/O和响应分离。独立服务、隔离不可信组件、独立重启。

“并发”表示多个任务在一段时间内推进,不保证同时执行;“并行”才涉及同时使用执行资源。线程数多于可用CPU核心仍可能有意义,例如等待I/O,但也可能增加上下文切换和竞争。

2. 构造时传函数,不要先调用它

跳转到“2. 构造时传函数,不要先调用它”

std::thread worker(function, argument)在新线程中调用函数。写成 std::thread worker(function(argument))会先在当前线程求值;如果函数返回 void,这个表达式根本无法构造线程。

完整例子如下:

#include <cassert>
#include <functional>
#include <thread>
void add(int& result, int amount)
{
result += amount;
}
int main()
{
int result = 0;
std::thread worker(add, std::ref(result), 100);
assert(worker.joinable());
worker.join();
assert(!worker.joinable());
assert(result == 100);
}

默认情况下,构造线程时会复制或移动保存传入的函数和参数;要传引用,使用 std::ref并保证被引用对象活得足够久。这里 result一直存在,而且主线程等待完成后才读取它,没有与工作线程同时读写。传指针并不自动复制它指向的对象。

可调用对象可以是普通函数、lambda、带 operator()的函数对象或成员函数与对象组合。成员函数示意为 std::thread(&Worker::run, &object, argument),其 object也必须活到调用结束。

3. joinable描述关联,不描述是否仍在运行

跳转到“3. joinable描述关联,不描述是否仍在运行”
操作/状态意义
默认构造的 std::thread没有关联线程,joinable()为false。
启动后,尚未join/detach有关联线程;即使函数已经返回,也仍可为joinable。
join()等待关联线程结束,完成后解除关联;同时为观察其结果提供同步。
detach()解除对象与执行线程的关联,之后无法通过此对象join它。
析构仍joinable的thread对象调用 std::terminate(),不是自动join或detach。

不能对没有关联线程的对象调用 join(),也不能让线程join自己。joinable()只是调用前的一项状态检查,不让并发操作同一个thread对象自动变得安全。C++工作草案:thread成员、析构

std::thread不可复制,可以移动。移动转移管理关联:移动后的源对象不再joinable;把新thread移动赋值到一个仍joinable的目标同样会终止程序,应先处理原关联。

正常路径末尾写一个 join(),仍可能被中间抛出的异常跳过。下面的有限例子用拥有线程的守卫,在离开作用域时等待。任务所引用的 result声明在守卫之前,因此其寿命覆盖守卫析构中的等待。

#include <cassert>
#include <stdexcept>
#include <thread>
#include <utility>
class JoiningThread {
public:
explicit JoiningThread(std::thread thread)
: thread_(std::move(thread)) {}
JoiningThread(const JoiningThread&) = delete;
JoiningThread& operator=(const JoiningThread&) = delete;
~JoiningThread()
{
if (thread_.joinable())
thread_.join();
}
private:
std::thread thread_;
};
int main()
{
int result = 0;
try {
JoiningThread worker(std::thread([&result] { result = 42; }));
throw std::runtime_error("main-thread operation failed");
} catch (const std::runtime_error&) {
assert(result == 42);
}
}

守卫必须由其他线程持有,不能在其管理的线程中销毁自身。它保证等待,不会让无限循环自动停止;如果工作线程等待一个永远不会再被满足的条件,析构也会一直等。建立停止标志、通知和退出条件是业务协议的一部分。

工作线程内抛出的异常不会自动传到创建者。让异常逃出线程入口会终止程序;应在线程入口内捕获并保存,或使用 std::promise/std::future、std::packaged_task等传递失败。C++20的 std::jthread提供自动join和协作停止接口,但也不会强行打断不配合停止的任务。

detach()之后,线程仍然需要有效的数据和可预测的结束机制。如果后台lambda捕获局部变量引用、栈对象地址或已经销毁的 this,就可能访问悬空对象。把thread对象分离并没有延长这些对象的寿命。

主线程从 main返回也不会自动等待所有已分离线程完成业务。需要结果、错误处理或正常退出顺序时,优先保留所有权并join,或交给明确拥有任务的线程池。只有真正独立、寿命有完整设计的后台任务才考虑分离。

6. 标识符、等待与调度提示

跳转到“6. 标识符、等待与调度提示”

std::this_thread是命名空间,包含当前线程相关函数;不是可以构造的类。

接口使用边界
thread.get_id()取得该对象关联线程的标识;无关联时为默认构造的 thread::id。
this_thread::get_id()取得当前执行线程标识,可做相等比较和日志。
this_thread::yield()调度提示,不保证另一指定线程马上运行。
this_thread::sleep_for(duration)等待一段时间,实际恢复可能更晚;不提供共享数据同步。
this_thread::sleep_until(timePoint)等待目标时刻,时钟种类与调整会影响语义。
thread::hardware_concurrency()可用硬件并发度的提示,可能返回0,不是最佳线程池大小保证。

线程ID不是“主线程等于1、后续线程按2、3编号”的标准保证,也不能依赖具体打印格式或线程结束后永不复用。

如果需求是“下一整分钟”,先明确使用墙钟;如果是“从现在起经过60秒”,通常使用单调的 steady_clock。下面只计算目标时间,避免示例运行时让读者等待一分钟:

#include <cassert>
#include <chrono>
int main()
{
using namespace std::chrono;
const auto now = system_clock::now();
const auto nextMinute = floor<minutes>(now) + minutes(1);
assert(nextMinute > now && nextMinute - now <= minutes(1));
// 若业务需要等待,可传给 std::this_thread::sleep_until(nextMinute)。
}

system_clock可能随校时变化;睡到整分钟也不保证在那个瞬间获得CPU。定时精度、截止时间和线程同步是三个不同问题,不能用随手增加 sleep_for替代正确的条件变量或互斥保护。