线程的基本操作

线程的基本操作

每个程序至少有一个线程:主线程(执行 main() 函数)。使用 std::thread 可以创建更多线程,它们与主线程并发运行

当线程完成任务,其对应的 std::thread 对象必须被妥善处理(如 join()detach()),否则程序会异常终止。

启动线程

启动线程的基本用法

void do_some_work();
std::thread my_thread(do_some_work);

创建 std::thread 对象时即启动线程。需包含 <thread> 头文件。

使用函数对象

class background_task {
public:
  void operator()() const {
    do_something();
    do_something_else();
  }
};

background_task f;
std::thread my_thread(f);

最令人头痛的语法解析(Most Vexing Parse)

std::thread my_thread(background_task()); // ❌ 实际上是函数声明!

看起来像是构造一个 std::thread 对象 my_thread,传入 background_task() 作为参数,但实际上,C++ 编译器会将它解析为函数声明,而不是对象定义。

从 C++ 编译器的视角来看,这个语法匹配了下面这种形式的函数声明:

返回类型       函数名     (       参数       );
----------    --------   --------------------
std::thread   my_thread  (background_task(*)())

即:这是一个函数声明,函数名是 my_thread,它的参数是一个函数指针,这个指针指向一个无参数、返回类型为 background_task 的函数,整个函数返回一个 std::thread 对象。

所以尽量避免写形如 Type name(Type()) 的代码。

避免方法:

使用额外一对括号
std::thread my_thread((background_task()));  // 使用双括号

外层括号强制编译器将其解释为构造函数参数,而不是函数声明。

使用统一初始化语法(C++11 起)
std::thread my_thread{background_task()};    // 使用统一初始化语法

大括号初始化不会被解释为函数声明。

使用 Lambda 表达式

std::thread my_thread([] {
  do_something();
  do_something_else();
});

等待线程完成(join)

线程运行后必须:

  • join():等待线程完成

  • detach():分离线程,后台运行

  • 否则程序崩溃(析构时报 std::terminate()

    • std::thread 的析构函数这么写的

      ~thread() {
          if (joinable()) {
              std::terminate(); // 崩溃:没处理这个线程
          }
      }
      
    • 也就是说:

      • 如果线程还 joinable(可连接),说明它没被处理;
      • 析构时如果忘了 join()detach(),就直接调用 std::terminate()

示例:线程访问了已经销毁的局部变量

struct func {
  int& i;
  func(int& i_) : i(i_) {}
  void operator() () {
    for (unsigned j = 0; j < 1000000; ++j) {
      do_something(i);  // 潜在访问无效引用
    }
  }
};

void oops() {
  int some_local_state = 0;
  func my_func(some_local_state);
  std::thread my_thread(my_func);
  my_thread.detach(); // ❌ 没有等待线程结束
} // ❌ 线程仍运行,但局部变量销毁
  • 主线程中 some_local_state 是局部变量,其生命周期到 oops() 函数结束为止。
  • 新线程通过引用访问它,如果未等待线程结束(即使用 detach()),新线程可能在局部变量销毁后仍尝试访问它。
  • 这是一种典型的多线程悬空引用(Dangling Reference)问题,可能导致程序崩溃或行为异常。
🧵 主线程执行步骤 🔀 新线程执行步骤
使用 some_local_state 构造 my_func
创建新线程 my_thread
启动线程,执行 func::operator()
do_something(i) 使用 some_local_state 的引用
调用 my_thread.detach()
some_local_state 被销毁 线程仍在运行,可能继续访问 some_local_state(已销毁)
函数 oops() 结束 若线程仍执行 do_something(i),则访问无效引用 → ❌ 未定义行为

特殊情况下的等待

线程异常期间也要确保调用 join()。否则会因为没有汇入线程导致程序崩溃。

示例:异常保护下的 join

struct func; // 同上

void f() {
  int some_local_state = 0;
  func my_func(some_local_state);
  std::thread t(my_func);
  try {
    do_something_in_current_thread();
  } catch (...) {
    t.join();  // ① 异常时确保线程结束
    throw;
  }
  t.join();    // ② 正常路径也等待线程结束
}

RAII风格的线程保护(推荐)

使用析构函数自动调用 join(),避免忘记或异常跳过。

class thread_guard {
  std::thread& t;
public:
  explicit thread_guard(std::thread& t_): t(t_) {}
  ~thread_guard() {
    if (t.joinable()) {
      t.join();  // ① 确保线程汇入
    }
  }
  thread_guard(thread_guard const&) = delete;       // ② 禁止拷贝
  thread_guard& operator=(thread_guard const&) = delete;
};

void f() {
  int some_local_state = 0;
  func my_func(some_local_state);
  std::thread t(my_func);
  thread_guard g(t);  // ③ RAII 保证安全
  do_something_in_current_thread();
} // ④ 离开作用域自动 join

后台运行线程(detach)

detach() 表示让线程在后台运行,即成为守护线程(daemon thread):

std::thread t(do_background_work);
t.detach();
assert(!t.joinable()); // 已分离

使用 t.joinable() 判断线程是否可被 join。

示例:多文档编辑器中使用分离线程

void edit_document(std::string const& filename) {
  open_document_and_display_gui(filename);
  while (!done_editing()) {
    user_command cmd = get_user_input();
    if (cmd.type == open_new_document) {
      std::string const new_name = get_filename_from_user();
      std::thread t(edit_document, new_name);  // ① 新线程打开新文档
      t.detach();  // ② 分离线程后台运行
    } else {
      process_user_input(cmd);
    }
  }
}

用户打开新文档时,创建新线程进行处理。每个文档处理线程互不干扰,适合使用分离线程。

对比

图解

主线程
│
├─ 创建 std::thread t(...)
│
├─ join() ─────────────→ 等待子线程执行完毕(同步)
│
└─ detach() ───────────→ 子线程后台独立执行(异步,自己跑)

基础定义对比

功能 join() detach()
含义 主动等待线程执行完毕 分离线程,让其独立后台运行
所属权 调用后线程归主线程管理 调用后线程归系统管理
阻塞行为 ✅ 阻塞当前线程,直到目标线程执行完 ❌ 不阻塞,立即返回
清理资源 ✅ 自动释放线程资源 ✅ 系统在线程结束时自动清理
是否 joinable() ❌ join 后不可 joinable ❌ detach 后不可 joinable

使用场景对比

场景 推荐操作 理由
需要等待线程结果 join() 等待线程执行完成,例如:下载完成、计算完成
子线程非关键、后台运行 detach() 无需等待线程,例如:日志、心跳、监控、缓存清理
创建线程但忘记处理 ❌ 程序崩溃 如果既不 join 也不 detach,析构时会 std::terminate()

注意事项对比

项目 join() detach()
是否可以多次调用 ❌ 不可以多次 join ❌ 不可以多次 detach
是否可以混用 ❌ join 和 detach 只能调用一次(或不调用) 否则程序崩溃
销毁前是否必须调用 ✅ 必须调用(join 或 detach 至少一个) ✅ 必须调用(否则析构崩溃)
线程安全性 安全,资源回收明确 ⚠️ 危险,需确保线程中不访问悬空对象
调试友好性 ✅ 调试方便,主线程可捕获异常或日志 ❌ 调试困难,后台线程奔溃你可能都不知道

总结

操作 含义与场景
join() 主动等待线程完成,释放资源,常规推荐方式
detach() 分离线程,后台运行,适用于 fire-and-forget
RAII 用类封装线程,自动在析构时调用 join()
避免错误 不要在线程中访问已销毁的局部变量引用
posted @ 2025-07-20 19:26  _Sylvan  阅读(36)  评论(0)    收藏  举报