C++17 多线程系列(一):线程基础——std::thread 完全指南

核心目标 :掌握 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() vs detach() 何时用?

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 attachinfo 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_lock
  • std::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 ------ 多线程调试
相关推荐
deepin_sir2 小时前
14 - 面向对象编程
开发语言·python
莫***妞2 小时前
2026年java后端开发还有未来吗? 就业形式如何?
java·开发语言
nickel3692 小时前
Qoder相关使用
java·开发语言·vue.js·spring boot
两年半的个人练习生^_^2 小时前
Java IO流之BIO
java·开发语言
wh_xia_jun2 小时前
HttpRunner 编写测试用例
开发语言·lua
吃好睡好便好2 小时前
提取矩阵所有元素
开发语言·学习·线性代数·matlab·矩阵
笨蛋不要掉眼泪2 小时前
Java并发编程:深入剖析 ArrayBlockingQueue
java·开发语言·算法·并发
吃好睡好便好2 小时前
提取矩阵特定多列元素
开发语言·学习·线性代数·matlab·矩阵
yujunl2 小时前
MES系统的悟道过程
开发语言