Chapter 2 归纳总结 — 线程管理

Chapter 2 归纳总结 --- 线程管理

来源:simonhancrew.github.io/CppConcuren... (对应《C++ Concurrency in Action》第 2 章 "Managing threads") 第 1 章讲了"为什么用并发",本章讲怎么把线程启动起来、管理好 : 构造即启动 → 等待(join)还是分离(detach)→ 传参 → 所有权移交 → 线程数量 → 线程标识。 最大的坑是局部变量的生命周期:线程比你函数活得久的烂主意(root of all evils)。

结构:2.1 线程基本操作 → 2.2 传递参数 → 2.3 转移所有权 → 2.4 确定线程数量 → 2.5 线程标识 → 2.6 本章总结。


1. 启动线程(2.1)

包含 <thread>,构造 std::thread 对象即启动(任务函数执行完线程结束)。

cpp 复制代码
void do_some_work();
std::thread my_thread(do_some_work);       // 传函数名

函数对象实例也可以(论上会复制到新线程的存储空间,在其中执行):

cpp 复制代码
class background_task {
public:
  void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);

★ 避掉 "最令人头痛的语法解析" (Most Vexing Parse)

cpp 复制代码
std::thread my_thread(background_task());  // 被解析为"函数声明",不是对象!
std::thread my_thread(background_task());   // 改多组括号 → 变成对象
std::thread my_thread{background_task()};  // 用 {} 统一初始化(C++11)
std::thread my_thread([]( ) {              // 或直接 lambda
  do_something(); do_something_else(); });

危险案例(清单 2.1):线程访问已销毁的局部变量

cpp 复制代码
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();         // ② 或 join 与否未定
}                             // ③ 若 detach,新线程可能还在跑,引用了已销毁的栈变量 → UB

核心:用访问局部变量的函数开线程 = 坏主意。 常规做法是把数据复制进线程 (按值)。 只要 std::thread 对象销毁前没调用 joindetach,析构函数会调用 std::terminate() 终止程序。


2. 等待线程:join()(2.1.2)

  • join():阻塞当前线程,等子线程结束,并回收线程相关资源
  • 注意 join 的位置 :如果 join() 之前抛出异常而跳过它,子线程还没回收 → 危险。

清单 2.2 try/catch 兜底 join

cpp 复制代码
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();      // ① 异常路径也 join
    throw;
  }
  t.join();        // ② 正常路径 join
}

清单 2.3 RAII 线程守卫(重点,推荐范式)

cpp 复制代码
class thread_guard {
  std::thread& t;
public:
  explicit thread_guard(std::thread& t_) : t(t_) {}
  ~thread_guard() {
    if(t.joinable())   // ① 先判断是否还能 join
      t.join();        // ② 析构时自动 join
  }
  thread_guard(const thread_guard&) = delete;            // ③ 防拷贝
  thread_guard& operator=(const thread_guard&) = delete;
};

void f() {
  int some_local_state = 0;
  func my_func(some_local_state);
  std::thread t(my_func);
  thread_guard g(t);                 // RAII:出了作用域自动 join
  do_something_in_current_thread();
}  // ④ 局部对象逆序销毁,先销毁 g → join 线程

RAII 精粹:把 join 放进析构 + joinable() 判空 + 拷贝构造拷贝赋值 =delete,异常也安全。


3. 后台运行:detach()(2.1.3)

  • 分离线程(daemon/守护线程) :后台独立运行,"发后即忘"(fire-and-forget)。分离后不可再 join
  • C++ 运行库保证线程退出时资源自动回收。
  • 调用前必须确认 joinable()(对无执行线程std::threaddetach 是错的)。
cpp 复制代码
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());   // 分离后不可 join

清单 2.4:用户编辑器/文档处理(分离线程处理任务)

cpp 复制代码
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);
    }
  }
}

分离线程与主线程无法直接交互;数据要安全地共享(见第 3 章)。


4. 向线程函数传递参数(2.2)

传参基本机制

  • 参数作为 std::thread 构造函数的附加参数,拷贝到新线程内存空间。
  • 即使函数参数是 引用,拷贝也会执行 ------ 拷贝发生在构造线程时,不是函数调用时(隐患见下)。
cpp 复制代码
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");   // 传 3 和字符串

悬空指针陷阱(关键)

cpp 复制代码
void oops(int some_param) {
  char buffer[1024];                        // ① 栈上数组
  sprintf(buffer, "%i", some_param);
  std::thread t(f, 3, buffer);              // ② 隐式 char* → 拷贝顺序不确定
  t.detach();                               // 线程可能在 buffer 销毁后还没完成隐式转换 → 悬空
}

COP与函数内隐式转换的顺序无法保证。 解决:显式转成 std::string 再传:

cpp 复制代码
std::thread t(f, 3, std::string(buffer));   // 值传递一个完整的 string

传引用:std::ref

直接传非常量引用变量会编译错(constructor 拷贝后以右值传参);用 std::ref 包住:

cpp 复制代码
void update_data_for_widget(widget_id w, widget_data& data);
std::thread t(update_data_for_widget, w, std::ref(data));

成员函数作线程函数(同 std::bind 语法)

cpp 复制代码
class X { public: void do_lengthy_work(int); };
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);   // 成员函数指针 + 对象指针

移动语义(只支持移动的类型)

cpp 复制代码
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));  // 必须显式 move

5. 转移所有权 std::thread(2.3)

  • std::thread 可移动、不可拷贝 (像 unique_ptr)。
  • 同一时刻一个实例只关联一个执行线程;移动后原对象变空(不能再 join/detach)。
cpp 复制代码
std::thread t1(func1);
std::thread t2 = std::move(t1);   // t1 所有权移到 t2,t1 空
t1 = std::thread(func2);          // 临时线程直接赋值(移动赋值,若 t1 可 join 会先 join,否则 terminate)

6. 确定线程数量 hardware_concurrency()(2.4)

cpp 复制代码
unsigned n = std::thread::hardware_concurrency();   // 硬件支持的并发线程数(核数),0 = 不可知
// 为 0 时要回退策略,比如自行选择默认值
  • 并行算法常只启动并行部分所需 的线程,通常不要多于核心数。过量线程 → 上下文切换过多 → 更慢(呼应 1.2.3)。

7. 线程标识 thread::id(2.5)

  • 类型 std::thread::id,可由 std::thread 成员 get_id()std::this_thread::get_id() 获取。
  • 无执行线程对象的 id 为默认构造的空 id,可判等。
  • std::thread::id 支持比较哈希,可作 map 的键 / 用于检测是否为主线程。
cpp 复制代码
std::thread::id main_id = std::this_thread::get_id();
std::thread::id t_id = t.get_id();
std::hash<std::thread::id> h;
if (t_id == main_id) { /* 我在主线程 */ }

8. 本章总结

主题 关键结论
启动 std::thread t(f, args...),构造即启动
join 等待线程结束;只能一次;异常安全 用 RAII thread_guard
detach 后台"发后即忘";先确认 joinable
传参 默认拷贝;引用用 std::ref;const* 明转 string 防悬空;move 传所有权
所有权 可 move 不可 copy,RAII/所有权移交
线程数 hardware_concurrency(),0 需回退
标识 thread::id 可比、可哈希

> thread_guard 是该书第一个反"dangling"的 RAII 组件,也是后续 unique_lock 的"前奏"。


与后续章节的衔接

  • 第 3 章:保护共享数据(mutex)------线程生命周期管理之后,轮到数据安全;
  • 第 4 章:同步操作(CV / future)---把本章的 join 升级为"等一个计算结果/事件";
  • 第 5 章:内存模型/原子 --- 真正揭示 thread 同步机制如何工作。

参考:仓库 doc/cia_ch1_concurrency_intro.mddoc/cia_ch4_sync_operations.mddoc/cia_ch5_atomic_summary.md# Chapter 2 归纳总结 --- 线程管理

来源:simonhancrew.github.io/CppConcuren... (对应《C++ Concurrency in Action》第 2 章 "Managing threads") 第 1 章讲了"为什么用并发",本章讲怎么把线程启动起来、管理好 : 构造即启动 → 等待(join)还是分离(detach)→ 传参 → 所有权移交 → 线程数量 → 线程标识。 最大的坑是局部变量的生命周期:线程比你函数活得久的烂主意(root of all evils)。

结构:2.1 线程基本操作 → 2.2 传递参数 → 2.3 转移所有权 → 2.4 确定线程数量 → 2.5 线程标识 → 2.6 本章总结。


1. 启动线程(2.1)

包含 <thread>,构造 std::thread 对象即启动(任务函数执行完线程结束)。

cpp 复制代码
void do_some_work();
std::thread my_thread(do_some_work);       // 传函数名

函数对象实例也可以(论上会复制到新线程的存储空间,在其中执行):

cpp 复制代码
class background_task {
public:
  void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);

★ 避掉 "最令人头痛的语法解析" (Most Vexing Parse)

cpp 复制代码
std::thread my_thread(background_task());  // 被解析为"函数声明",不是对象!
std::thread my_thread(background_task());   // 改多组括号 → 变成对象
std::thread my_thread{background_task()};  // 用 {} 统一初始化(C++11)
std::thread my_thread([]( ) {              // 或直接 lambda
  do_something(); do_something_else(); });

危险案例(清单 2.1):线程访问已销毁的局部变量

cpp 复制代码
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();         // ② 或 join 与否未定
}                             // ③ 若 detach,新线程可能还在跑,引用了已销毁的栈变量 → UB

核心:用访问局部变量的函数开线程 = 坏主意。 常规做法是把数据复制进线程 (按值)。 只要 std::thread 对象销毁前没调用 joindetach,析构函数会调用 std::terminate() 终止程序。


2. 等待线程:join()(2.1.2)

  • join():阻塞当前线程,等子线程结束,并回收线程相关资源
  • 注意 join 的位置 :如果 join() 之前抛出异常而跳过它,子线程还没回收 → 危险。

清单 2.2 try/catch 兜底 join

cpp 复制代码
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();      // ① 异常路径也 join
    throw;
  }
  t.join();        // ② 正常路径 join
}

清单 2.3 RAII 线程守卫(重点,推荐范式)

cpp 复制代码
class thread_guard {
  std::thread& t;
public:
  explicit thread_guard(std::thread& t_) : t(t_) {}
  ~thread_guard() {
    if(t.joinable())   // ① 先判断是否还能 join
      t.join();        // ② 析构时自动 join
  }
  thread_guard(const thread_guard&) = delete;            // ③ 防拷贝
  thread_guard& operator=(const thread_guard&) = delete;
};

void f() {
  int some_local_state = 0;
  func my_func(some_local_state);
  std::thread t(my_func);
  thread_guard g(t);                 // RAII:出了作用域自动 join
  do_something_in_current_thread();
}  // ④ 局部对象逆序销毁,先销毁 g → join 线程

RAII 精粹:把 join 放进析构 + joinable() 判空 + 拷贝构造拷贝赋值 =delete,异常也安全。


3. 后台运行:detach()(2.1.3)

  • 分离线程(daemon/守护线程) :后台独立运行,"发后即忘"(fire-and-forget)。分离后不可再 join
  • C++ 运行库保证线程退出时资源自动回收。
  • 调用前必须确认 joinable()(对无执行线程std::threaddetach 是错的)。
cpp 复制代码
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());   // 分离后不可 join

清单 2.4:用户编辑器/文档处理(分离线程处理任务)

cpp 复制代码
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);
    }
  }
}

分离线程与主线程无法直接交互;数据要安全地共享(见第 3 章)。


4. 向线程函数传递参数(2.2)

传参基本机制

  • 参数作为 std::thread 构造函数的附加参数,拷贝到新线程内存空间。
  • 即使函数参数是 引用,拷贝也会执行 ------ 拷贝发生在构造线程时,不是函数调用时(隐患见下)。
cpp 复制代码
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");   // 传 3 和字符串

悬空指针陷阱(关键)

cpp 复制代码
void oops(int some_param) {
  char buffer[1024];                        // ① 栈上数组
  sprintf(buffer, "%i", some_param);
  std::thread t(f, 3, buffer);              // ② 隐式 char* → 拷贝顺序不确定
  t.detach();                               // 线程可能在 buffer 销毁后还没完成隐式转换 → 悬空
}

COP与函数内隐式转换的顺序无法保证。 解决:显式转成 std::string 再传:

cpp 复制代码
std::thread t(f, 3, std::string(buffer));   // 值传递一个完整的 string

传引用:std::ref

直接传非常量引用变量会编译错(constructor 拷贝后以右值传参);用 std::ref 包住:

cpp 复制代码
void update_data_for_widget(widget_id w, widget_data& data);
std::thread t(update_data_for_widget, w, std::ref(data));

成员函数作线程函数(同 std::bind 语法)

cpp 复制代码
class X { public: void do_lengthy_work(int); };
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);   // 成员函数指针 + 对象指针

移动语义(只支持移动的类型)

cpp 复制代码
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));  // 必须显式 move

5. 转移所有权 std::thread(2.3)

  • std::thread 可移动、不可拷贝 (像 unique_ptr)。
  • 同一时刻一个实例只关联一个执行线程;移动后原对象变空(不能再 join/detach)。
cpp 复制代码
std::thread t1(func1);
std::thread t2 = std::move(t1);   // t1 所有权移到 t2,t1 空
t1 = std::thread(func2);          // 临时线程直接赋值(移动赋值,若 t1 可 join 会先 join,否则 terminate)

6. 确定线程数量 hardware_concurrency()(2.4)

cpp 复制代码
unsigned n = std::thread::hardware_concurrency();   // 硬件支持的并发线程数(核数),0 = 不可知
// 为 0 时要回退策略,比如自行选择默认值
  • 并行算法常只启动并行部分所需 的线程,通常不要多于核心数。过量线程 → 上下文切换过多 → 更慢(呼应 1.2.3)。

7. 线程标识 thread::id(2.5)

  • 类型 std::thread::id,可由 std::thread 成员 get_id()std::this_thread::get_id() 获取。
  • 无执行线程对象的 id 为默认构造的空 id,可判等。
  • std::thread::id 支持比较哈希,可作 map 的键 / 用于检测是否为主线程。
cpp 复制代码
std::thread::id main_id = std::this_thread::get_id();
std::thread::id t_id = t.get_id();
std::hash<std::thread::id> h;
if (t_id == main_id) { /* 我在主线程 */ }

8. 本章总结

主题 关键结论
启动 std::thread t(f, args...),构造即启动
join 等待线程结束;只能一次;异常安全 用 RAII thread_guard
detach 后台"发后即忘";先确认 joinable
传参 默认拷贝;引用用 std::ref;const* 明转 string 防悬空;move 传所有权
所有权 可 move 不可 copy,RAII/所有权移交
线程数 hardware_concurrency(),0 需回退
标识 thread::id 可比、可哈希

> thread_guard 是该书第一个反"dangling"的 RAII 组件,也是后续 unique_lock 的"前奏"。


与后续章节的衔接

  • 第 3 章:保护共享数据(mutex)------线程生命周期管理之后,轮到数据安全;
  • 第 4 章:同步操作(CV / future)---把本章的 join 升级为"等一个计算结果/事件";
  • 第 5 章:内存模型/原子 --- 真正揭示 thread 同步机制如何工作。

参考:仓库 doc/cia_ch1_concurrency_intro.mddoc/cia_ch4_sync_operations.mddoc/cia_ch5_atomic_summary.md

相关推荐
Zane19941 小时前
从一个发短信的类到多态调用:封装、继承、多态到底是怎么长出来的
java·后端
vipxieliang2 小时前
ValidX 迁移指南:v1.0.0/v1.0.1 → v1.1.0
后端
未秃头的程序猿2 小时前
从写CRUD到做AI Agent:我花了6个月转型,这是我的完整路线图
java·后端·ai编程
Java内核笔记2 小时前
万字长文剖析 Spring Boot 4.1.0 启动流程源码:从 main 到就绪
java·后端
leoZ2312 小时前
Vue3 还原一个企业级后台-14-项目总结
开发语言·人工智能·后端·opencv·计算机视觉·数据挖掘·rust
坤岭3 小时前
企业级Agent从0到1
后端
用户69371750013843 小时前
DeepSeek 调价正式生效:一夜涨 11 倍,靠低价薅羊毛的日子结束了
前端·人工智能·后端
用户298698530143 小时前
从入门到自动化:TXT 转 Word 的在线工具与代码实战方案
人工智能·后端·python
RainCityLucky3 小时前
Java Swing 自定义组件库分享(十六)
java·笔记·后端