核心目标 :掌握
std::thread的创建、管理和销毁,理解线程生命周期,建立多线程编程的第一块基石。前置知识:C++11/14 基础语法(lambda、move 语义);了解操作系统进程/线程基本概念更佳。
1.1 第一个多线程程序
1.1.1 Hello World 多线程版
cpp
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread #"
<< std::this_thread::get_id() << "\n";
}
int main() {
std::thread t(hello); // 创建线程,立即开始执行 hello()
t.join(); // 等待线程执行完毕
std::cout << "Hello from main thread\n";
}
编译与运行:
bash
# ⚠️ 链接 pthread 库(Linux 必需)
g++ -std=c++17 -pthread -o hello_thread hello.cpp
./hello_thread
# 输出示例:
# Hello from thread #140215367804672
# Hello from main thread
1.1.2 单线程 vs 多线程执行模型
并行改造
多线程模型
主线程: Task A ──────────────
Thread 1: Task B ────────
Thread 2: Task C ────
单线程模型
Task A
Task B
Task C
1.2 std::thread 的 5 种构造方式
1.2.1 方式一:普通函数指针
cpp
void do_work(int n) {
std::cout << "Working on " << n << "\n";
}
std::thread t1(do_work, 42); // 自动推导参数
t1.join();
1.2.2 方式二:函数对象(Functor)
cpp
class Worker {
public:
void operator()(int n) const {
std::cout << "Worker processing " << n << "\n";
}
};
Worker w;
std::thread t2(w, 100); // 传递 w 的副本
t2.join();
// ⚠️ C++'s Most Vexing Parse 陷阱:
// std::thread t2(Worker()); // ❌ 这被解析为函数声明!
// 正确写法:
// std::thread t2{Worker()}; // ✅ 大括号初始化
// std::thread t2((Worker())); // ✅ 额外括号
1.2.3 方式三:Lambda 表达式(最常用)
cpp
int x = 42;
int y = 10;
std::thread t3([x, &y]() {
// x: 按值捕获(线程安全)
// y: 按引用捕获(⚠️ 注意生命周期!)
std::cout << "x=" << x << ", y=" << y << "\n";
});
t3.join();
1.2.4 方式四:成员函数指针 + 对象
cpp
class Task {
public:
void run(const std::string& name) {
std::cout << "Task " << name << " running\n";
}
};
Task task;
std::thread t4(&Task::run, &task, "Batch-01"); // 对象地址 + 成员函数
t4.join();
// 也可以用 std::ref 传递
std::thread t5(&Task::run, std::ref(task), "Batch-02");
t5.join();
1.2.5 方式五:带多个参数
cpp
void process(int a, double b, const std::string& c) {
std::cout << a << ", " << b << ", " << c << "\n";
}
std::thread t6(process, 1, 3.14, std::string("hello"));
t6.join();
| 构造方式 | 适用场景 | 注意事项 |
|---|---|---|
| 函数指针 | 已有独立函数 | 参数按值传递 |
| Functor | 有状态的任务对象 | 避免 Most Vexing Parse |
| Lambda | 最灵活、最常用 | 注意引用捕获的生命周期 |
| 成员函数 | 调用对象方法 | 传递对象指针或 std::ref |
| 多参数 | 任意组合 | 参数类型必须匹配 |
1.3 线程生命周期管理
1.3.1 线程状态机
时间片耗尽
等待 I/O / 锁
资源就绪
join()
detach()
构造 std::thread
NEW
线程对象创建
RUNNABLE
就绪: 等待调度
RUNNING
执行中
BLOCKED
阻塞等待
JOINED
已汇合
DETACHED
已分离
销毁
进程退出时销毁
1.3.2 join() ------ 等待线程结束
cpp
void compute() {
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::thread t(compute);
// join() 阻塞当前线程,直到 t 执行完毕
t.join();
// 此后 t.joinable() == false
// ❌ 不能再次 join
// t.join(); // std::system_error
1.3.3 detach() ------ 分离线程
cpp
void background_work() {
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "Background work done\n";
}
std::thread t(background_work);
t.detach(); // 线程在后台独立运行
// ⚠️ detach 后无法再控制该线程
// t.joinable() == false
// t.join(); // ❌ 已 detach
join()vsdetach()何时用?
join() detach() 需要等待线程结果 纯后台任务 主线程依赖子线程 生命周期完全独立 推荐优先使用 守护线程
1.3.4 RAII 包装:thread_guard
cpp
// ⚠️ 异常安全问题:如果 join() 之前抛异常,析构会 terminate
void unsafe_function() {
std::thread t([]{ /* work */ });
do_something_might_throw(); // 抛异常 → t 未 join → terminate!
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(); // 确保 join
}
}
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
void safe_function() {
std::thread t([]{ /* work */ });
thread_guard guard(t); // 异常安全!
do_something_might_throw(); // 即使抛异常, 析构函数会 join
}
铁律 :每个
std::thread对象在销毁前,要么join(),要么detach(),否则std::terminate。
1.4 线程标识
cpp
#include <thread>
#include <sstream>
void print_thread_info() {
auto id = std::this_thread::get_id();
std::ostringstream oss;
oss << id; // 可以序列化为字符串
std::cout << "Thread ID: " << oss.str() << "\n";
}
TEST(ThreadId, Comparison) {
std::thread::id main_id = std::this_thread::get_id();
std::thread::id child_id;
std::thread t([&child_id] {
child_id = std::this_thread::get_id();
});
t.join();
EXPECT_NE(main_id, child_id); // 不同线程 ID 不同
EXPECT_EQ(std::thread::id(), std::thread::id()); // 默认 ID 相等
}
Linux 下为线程命名 (方便
top -H/ GDB 识别):
cpp#include <pthread.h> pthread_setname_np(pthread_self(), "Worker-01");
1.5 线程数量控制
1.5.1 获取硬件并发数
cpp
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << n << "\n";
// 8 核 16 线程的 CPU 通常返回 16
}
1.5.2 如何确定最佳线程数
CPU 密集型(计算为主):
线程数 = hardware_concurrency() // 多一个都不好
IO 密集型(等待网络/磁盘):
线程数 = hardware_concurrency() × 2~4 // 等待时其他线程可运行
混合型:
线程数 = hardware_concurrency() / (1 - 阻塞系数)
例如阻塞 50% → 线程数 = cores / 0.5 = 2 × cores
cpp
#include <thread>
#include <vector>
void cpu_bound_work() {
unsigned int threads = std::thread::hardware_concurrency();
std::vector<std::thread> workers;
for (unsigned int i = 0; i < threads; ++i) {
workers.emplace_back([i] {
// 每个线程处理一部分数据
heavy_computation(i);
});
}
for (auto& t : workers) t.join();
}
1.6 线程局部存储(thread_local)
cpp
#include <random>
#include <thread>
// ★ 每个线程拥有独立的 random engine 实例
thread_local std::mt19937 rng(std::random_device{}());
thread_local std::uniform_int_distribution<int> dist(1, 100);
void generate_random_numbers() {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << std::this_thread::get_id()
<< ": " << dist(rng) << "\n";
}
}
int main() {
std::thread t1(generate_random_numbers);
std::thread t2(generate_random_numbers);
t1.join();
t2.join();
// 两个线程的随机数序列完全独立
}
thread_local vs 其他存储周期对比:
| 存储类型 | 每线程独立? | 生命周期 | 典型场景 |
|---|---|---|---|
static |
❌ 全局共享 | 程序全周期 | 全局配置 |
函数内 static |
❌ 所有线程共享 | 首次调用到程序结束 | 懒加载单例 |
thread_local |
✅ 每线程独立 | 线程全周期 | 随机数引擎、错误状态 |
| 局部变量 | ✅ 每线程独立 | 函数调用周期 | 临时计算结果 |
1.7 传递参数给线程
1.7.1 默认按值拷贝
cpp
void f(int i, const std::string& s) {
std::cout << i << ": " << s << "\n";
}
std::thread t(f, 42, "hello");
// "hello" (const char*) → 在线程上下文中隐式转换为 std::string
t.join();
1.7.2 引用传递陷阱
cpp
void update_big_data(std::vector<int>& data) {
data.push_back(99);
}
std::vector<int> buffer(1000000);
// ❌ 编译错误! std::thread 默认拷贝参数
// std::thread t(update_big_data, buffer);
// ✅ 使用 std::ref 传递引用
std::thread t(update_big_data, std::ref(buffer));
t.join();
assert(buffer.back() == 99); // 验证引用传递生效
1.7.3 指针参数与生命周期陷阱
cpp
void dangerous_example() {
int local_var = 42;
std::thread t([&local_var] { // ⚠️ 按引用捕获!
std::this_thread::sleep_for(std::chrono::seconds(1));
// 此时 local_var 可能已被销毁!
std::cout << local_var; // 悬空引用 = 未定义行为!
});
t.detach();
} // local_var 销毁, 但线程还在运行!
// ✅ 修复: 按值捕获
void safe_example() {
int local_var = 42;
std::thread t([local_var] { // 按值拷贝, 安全!
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << local_var;
});
t.detach();
}
1.7.4 std::ref / std::cref 速查
cpp
std::vector<int> data(1000);
std::thread t1(process, data); // 拷贝整个 vector
std::thread t2(process, std::ref(data)); // 传递引用,不拷贝
std::thread t3(read_only, std::cref(data)); // const 引用(只读保证)
1.8 线程休眠与让步
cpp
#include <chrono>
// sleep_for: 休眠指定时间段
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::seconds(2));
// sleep_until: 休眠到指定时间点
auto wake_time = std::chrono::steady_clock::now() + std::chrono::seconds(5);
std::this_thread::sleep_until(wake_time);
// yield: 主动让出 CPU 时间片
// ⚠️ 策略性使用,不要作为同步手段!
std::this_thread::yield();
yield() 的使用场景与误区:
cpp
// ❌ 错误: 用 yield 做忙等待 (busy-wait)
while (!ready) {
std::this_thread::yield(); // 浪费 CPU,应该用 condition_variable!
}
// ✅ 正确: yield 仅用于优化自旋锁的最后一步
// 或当你知道另一个线程正在等待时短时让步
1.9 常见错误与调试
错误 1:忘记 join → std::terminate
cpp
// ❌ 运行时崩溃
void bug_01() {
std::thread t([]{ /* work */ });
// 函数结束,t 的析构函数发现 joinable()==true → terminate!
}
// ✅ 修复
void fix_01() {
std::thread t([]{ /* work */ });
t.join(); // 或 detach()
}
错误 2:主线程提前退出
cpp
// ❌ 子线程访问已销毁的局部变量
void bug_02() {
int local = 42;
std::thread t([&local] { // 引用捕获
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << local; // 主线程可能已退出,local 已销毁!
});
t.detach();
}
// ✅ 修复: 拷贝 or 确保 join
void fix_02() {
int local = 42;
std::thread t([local] { // 按值捕获
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << local; // 安全: local 是副本
});
t.detach();
}
错误 3:多线程同时写 std::cout(无同步)
cpp
// ❌ 输出交错混乱
void bug_03() {
auto print = [] {
for (int i = 0; i < 10; ++i) {
std::cout << i << " "; // 多线程写入同一流!
}
};
std::thread t1(print), t2(print);
t1.join(); t2.join();
}
// 可能输出: 0 0 1 1 2 2 3 3 ... (交错混乱)
// ✅ 临时修复: 把输出攒到一个 string 再 cout
void fix_03() {
auto print = [] {
std::ostringstream oss;
for (int i = 0; i < 10; ++i) oss << i << " ";
std::cout << oss.str(); // 单次写入
};
std::thread t1(print), t2(print);
t1.join(); t2.join();
}
错误诊断速查表
| 症状 | 可能原因 | 排查工具 |
|---|---|---|
std::terminate 无异常信息 |
忘记 join/detach | GDB backtrace |
| 随机崩溃 | 引用捕获 + 生命周期 | Address Sanitizer |
| 输出交错混乱 | 多线程写同一流 | 肉眼 / ` |
| 程序卡死不退出 | join 死等 + 未 detach | gdb attach → info threads |
1.10 小结
| 知识点 | 掌握程度 | 核心要点 |
|---|---|---|
| 5 种 thread 构造方式 | 熟练 | Lambda 最常用,注意 Most Vexing Parse |
| join vs detach | 掌握 | joinable() 为 true 时必须处理,否则 terminate |
| RAII thread_guard | 理解 | 异常安全的保证 |
| 线程 ID | 理解 | get_id() + 为线程命名 |
| hardware_concurrency | 掌握 | CPU密集型 = 核数,IO密集型 = 2-4×核数 |
| thread_local | 掌握 | 每线程独立变量,适合随机种子/错误状态 |
| 参数传递 | 掌握 | 默认拷贝,引用用 std::ref |
| 生命周期陷阱 | 掌握 | detach + 引用捕获 = 悬空引用 = UB |
| sleep vs yield | 理解 | sleep 用于等待,yield 谨慎使用 |
下期预告
[Part 2:共享数据与同步] 将深入 mutex 和 condition_variable:
std::mutex/std::lock_guard/std::unique_lockstd::scoped_lock(C++17) 多锁 RAII- 死锁的四条件与避免策略
std::condition_variable生产者-消费者模式- 锁粒度对性能的影响
推荐工具
-std=c++17 -pthread------ 编译多线程程序的基本标志-fsanitize=thread -g------ Thread Sanitizer 检测 data race- GDB
info threads+thread apply all bt------ 多线程调试