c++并发

1.std::thread

1.1.头文件与基本概念

c 复制代码
#include <thread>

std::thread 是 C++11 引入的线程类,代表单个执行线程。每个 std::thread 对象要么:

  • 关联一个实际操作系统线程(joinable)
  • 不关联任何线程(non-joinable / not-a-thread)

1.2.构造函数

构造函数 说明 可用性
thread() noexcept 默认构造,不关联任何线程
thread(thread&& other) noexcept 移动构造,转移线程所有权
thread(const thread&) = delete 拷贝构造被删除,线程不可拷贝
template<class F, class... Args> thread(F&& f, Args&&... args) 有参构造 ,启动新线程执行 f(args...)

1.3.赋值操作

操作 说明
thread& operator=(thread&& other) noexcept 移动赋值,转移线程所有权
thread& operator=(const thread&) = delete 拷贝赋值被删除

移动赋值规则

c 复制代码
std::thread t1(func);
std::thread t2;

t2 = std::move(t1);  // OK:t1 的所有权转移给 t2

// 注意:如果被赋值方(t2)当前是 joinable 的,调用 terminate()

⚠️ 关键陷阱:如果赋值目标当前是 joinable 的,程序会调用 std::terminate() 终止!

1.4.开启新线程的方式

1.4.1.普通函数

c 复制代码
void task(int id) { /* ... */ }
std::thread t(task, 42);

1.4.2.Lambda 表达式(最常用)

c 复制代码
std::thread t([]() {
    std::cout << "Hello from thread\n";
});

1.4.3.仿函数(Function Object)

c 复制代码
struct Task {
    void operator()(int x) const { /* ... */ }
};
std::thread t(Task(), 100);

1.4.4.成员函数

c 复制代码
class Worker {
public:
    void doWork(int param) { /* ... */ }
};

Worker w;
std::thread t(&Worker::doWork, &w, 42);  // 第1个参数是成员函数指针,第2个是对象指针/引用

1.4.5.bind 绑定

c 复制代码
using namespace std::placeholders;
std::thread t(std::bind(func, 10, _1), 20);

1.5.通过传参开启新线程及内部原理

1.5.1.基本用法

c 复制代码
void process(int n, const std::string& str, double* ptr);

std::thread t(process, 100, "hello", &value);

1.5.2.内部原理:Decay 拷贝 + 完美转发

c 复制代码
template<class F, class... Args>
thread(F&& f, Args&&... args) {
    // 伪代码:
    // 1. 所有参数进行 decay-copy(值拷贝)
    // 2. 在新线程内部,用完美转发调用 f(decay_copy(args)...)
}

核心机制:

机制 说明
Decay-copy 参数会先被拷贝到线程内部存储,即使原参数是引用。这是为了防止悬垂引用(dangling reference)
完美转发 在新线程启动时,用 std::forward 将存储的参数传递给目标函数
类型退化 (decay) 数组退化为指针,函数退化为函数指针,const/volatile 和引用被移除

1.5.3.传参陷阱与解决方案

1.陷阱 1:引用参数会被拷贝

c 复制代码
void update(int& x) { x = 42; }

int value = 0;
std::thread t(update, value);  // ❌ 编译错误!value 被 decay 为 int,不是 int&

解决方案:使用 std::ref / std::cref

c 复制代码
std::thread t(update, std::ref(value));  // ✓ 显式传递引用
t.join();
// value 现在是 42

2.陷阱 2:指针/引用指向局部变量

c 复制代码
void bad() {
    int local = 10;
    std::thread t([](int* p) { *p = 20; }, &local);
    t.detach();  // ❌ 灾难!线程可能在函数返回后才执行,local 已销毁
}

陷阱 3:字符串字面量被 decay 为 const char*

c 复制代码
void func(std::string s);

std::thread t(func, "hello");  // OK:const char* 可以构造 string
// 但如果函数参数是 const char*,而传入 string 对象,可能不匹配

陷阱 4:类成员函数传参

c 复制代码
class Foo {
public:
    void bar(int x, std::string s);
};

Foo foo;
std::thread t(&Foo::bar, &foo, 42, "hello");
// 参数分解:(&foo)->bar(42, "hello")

1.6.线程所有权转移 (Move Semantics)

std::thread 是资源句柄(类似 std::unique_ptr),其所有权可以移动但不能拷贝。

1.6.1.所有权转移场景

c 复制代码
std::thread createThread() {
    std::thread t([]() {
        std::cout << "In new thread\n";
    });
    return t;  // ✓ 隐式移动(NRVO / 移动语义)
}

int main() {
    std::thread t = createThread();  // 所有权从函数转移到 t
    t.join();
}

1.6.2.存入容器

c 复制代码
std::vector<std::thread> workers;

for (int i = 0; i < 5; ++i) {
    workers.emplace_back([i]() {  // ✓ emplace_back 直接构造,避免拷贝
        std::cout << "Worker " << i << "\n";
    });
}

for (auto& t : workers) {
    if (t.joinable()) t.join();
}

1.6.3.RAII 包装(最佳实践)

由于 std::thread 析构时如果是 joinable 会调用 terminate(),通常需要 RAII 包装:

c 复制代码
class ThreadGuard {
    std::thread& t;
public:
    explicit ThreadGuard(std::thread& _t) : t(_t) {}
    
    ~ThreadGuard() {
        if (t.joinable()) {
            t.join();  // 或 t.detach(),视需求而定
        }
    }
    
    ThreadGuard(const ThreadGuard&) = delete;
    ThreadGuard& operator=(const ThreadGuard&) = delete;
};

// 使用
std::thread t(func);
ThreadGuard guard(t);  // 保证异常安全

1.7.重要成员函数

函数 说明
join() 阻塞等待线程完成
detach() 分离线程,不再关联,线程独立运行
joinable() 检查是否关联活跃线程
get_id() 返回线程 ID
native_handle() 返回底层 OS 线程句柄(平台相关)
swap(thread&) 交换两个 thread 对象
hardware_concurrency() 静态 返回支持的并发线程数

1.8.完整综合示例

c 复制代码
#include <thread>
#include <iostream>
#include <vector>
#include <functional>

void worker(int id, int& result, std::mutex& mtx) {
    int local = id * 10;
    
    std::lock_guard<std::mutex> lock(mtx);
    result += local;
    std::cout << "Worker " << id << " done\n";
}

int main() {
    int total = 0;
    std::mutex mtx;
    std::vector<std::thread> threads;
    
    // 创建多个线程,传递引用参数
    for (int i = 1; i <= 3; ++i) {
        threads.emplace_back(worker, i, std::ref(total), std::ref(mtx));
    }
    
    // 所有权在 vector 中,统一 join
    for (auto& t : threads) {
        if (t.joinable()) t.join();
    }
    
    std::cout << "Total: " << total << "\n";  // Total: 60
    return 0;
}

1.9.关键要点总结

  • 不可拷贝:std::thread 只能移动,不能拷贝
  • 参数 decay:默认所有参数按值拷贝到线程内部,引用需用 std::ref
  • 所有权管理:移动语义支持容器存储和函数返回
  • join/detach 二选一:析构前必须调用其一,否则 terminate()

1.10.decay与传参专项

很多人误以为 std::thread(func, arg1, arg2) 只是简单地把参数完美转发给 func。实际上,标准规定了一个两步机制:

c 复制代码
主线程                              新线程
─────────────────────────────────────────────────────────
thread(func, arg1, arg2)
       │
       ▼
┌─────────────────┐
│  Step 1: 参数被  │  这些副本存储在线程
│  "decay-copy"   │  对象内部的存储空间中
│  创建副本       │
└─────────────────┘
       │
       ▼
   [ 新线程启动 ] ──────────► ┌─────────────────┐
                              │  Step 2: 用这些  │
                              │  副本调用 func   │
                              │  (完美转发)      │
                              └─────────────────┘

1.10.1.什么是 Decay-Copy?

标准中的伪代码逻辑:

c 复制代码
template<class F, class... Args>
thread(F&& f, Args&&... args) {
    // 1. 对每个参数做 decay-copy,存储在线程内部
    auto stored_args = std::make_tuple(
        decay_copy(std::forward<Args>(args))...
    );
    // 2. 新线程启动时,用存储的副本调用 f
    // invoke(f, get<0>(stored_args), get<1>(stored_args), ...)
}

其中 decay_copy(x) 等价于:

c 复制代码
typename std::decay<decltype(x)>::type tmp(x);  // 按值拷贝构造

std::decay 会做什么?

原始类型 Decay 后类型 影响
int& int 引用被移除!
const int& int const 和引用都被移除
int[5] int* 数组退化为指针!
void(int) void(*)(int) 函数退化为函数指针
volatile int int volatile 被移除

1.10.2.为什么必须 Decay-Copy?------ 生命周期安全

反例:如果直接传引用

c 复制代码
void update(int& x) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    x = 42;  // 写入引用
}

void danger() {
    int local = 0;
    std::thread t(update, local);  // 假设这里直接传引用
    t.detach();
    // danger() 返回,local 被销毁!
    // 但新线程还在 sleep,醒来后发现 local 已经不存在了
    // → 悬垂引用 (Dangling Reference) → 未定义行为 (UB)
}

C++ 标准委员会的设计决策:默认必须安全。如果用户真的需要引用,必须显式表达这个意图(用 std::ref),并自行承担生命周期管理责任。

1.10.3.std::ref 的工作原理

1.10.3.1.std::reference_wrapper 的本质

std::ref(x) 返回一个 std::reference_wrapper,它不是引用,而是一个可拷贝的轻量对象:

c 复制代码
// 简化版实现
template<class T>
class reference_wrapper {
    T* ptr;  // 注意:存的是指针,不是引用!
public:
    reference_wrapper(T& t) : ptr(std::addressof(t)) {}
    
    // 可以隐式转换回引用
    operator T& () const { return *ptr; }
    T& get() const { return *ptr; }
    
    // 支持拷贝(因为 ptr 可以拷贝)
    reference_wrapper(const reference_wrapper&) = default;
};
1.10.3.2.使用 std::ref 后的完整流程
c 复制代码
void foo(int& x) { x = 100; }

int main() {
    int a = 0;
    
    // 发生了什么?
    std::thread t(foo, std::ref(a));
    //           │        │
    //           │        └─► 创建 reference_wrapper<int>(&a)
    //           │              这是一个对象,可以安全 decay-copy
    //           │
    //           └─► 新线程内部:reference_wrapper 隐式转为 int&
    //               实际调用 foo(a) ------ 真正的引用!
    
    t.join();
    std::cout << a;  // 100 ✓
}

关键洞察:

  • std::ref(a) 产生的对象被值拷贝到线程内部(安全,因为它只是个小对象)
  • 但这个小对象内部持有指向 a 的指针
  • 在新线程中调用时,它把指针解引用,还原成对原变量的引用

1.10.4.代码对比实验

实验 1:不加 std::ref ------ 编译失败

c 复制代码
#include <thread>

void increment(int& x) {
    ++x;
}

int main() {
    int value = 0;
    std::thread t(increment, value);  // ❌ 编译错误!
    /*
    错误信息大致是:
    error: no matching constructor...
    note:   candidate expects different arguments...
    
    原因:value (int&) 被 decay 成了 int,
          但 increment 期望 int&,类型不匹配
    */
    t.join();
}

实验 2:加 std::ref ------ 成功

c 复制代码
#include <thread>
#include <functional>  // for std::ref

void increment(int& x) {
    ++x;
}

int main() {
    int value = 0;
    std::thread t(increment, std::ref(value));  // ✓ 编译通过
    t.join();
    // value == 1
}

实验 3:数组的 Decay

c 复制代码
#include <thread>
#include <iostream>

void print_array(int arr[5]) {  // 实际上等价于 int* arr
    std::cout << sizeof(arr);   // 输出指针大小 (8),不是 20!
}

int main() {
    int data[5] = {1,2,3,4,5};
    std::thread t(print_array, data);  // data 被 decay 为 int*
    t.join();
}

实验 4:const 被移除

c 复制代码
void modify(int x) {  // 传值
    x = 999;  // 只修改副本
}

int main() {
    const int value = 42;
    std::thread t(modify, value);  // value 被 decay 为 int,const 被移除
    t.join();
    // value 仍然是 42
}

1.10.5.总结表

你的代码 实际存储在线程内部 新线程中调用时
std::thread(f, int_var) int 的副本 传这个 int 副本
std::thread(f, std::ref(int_var)) reference_wrapper<int> 的副本(内含指针) 解引用为 int&
std::thread(f, std::cref(int_var)) reference_wrapper<const int> 的副本 解引用为 const int&
std::thread(f, array) 数组首地址指针 传指针
std::thread(f, lambda) lambda 的副本(或移动后的副本) 传 lambda

1.10.6.最佳实践

c 复制代码
#include <thread>
#include <functional>
#include <vector>

// ✅ 需要修改原变量 → 用 std::ref
void update(std::vector<int>& data) {
    data.push_back(42);
}

// ✅ 只读访问大对象 → 用 std::cref(避免拷贝)
void read_only(const std::vector<int>& data) {
    // 读取 data...
}

// ✅ 线程安全的值传递 → 直接传
void process(int value) {
    // 用 value 的副本工作
}

int main() {
    std::vector<int> vec = {1, 2, 3};
    int x = 10;
    
    std::thread t1(update, std::ref(vec));        // 修改原 vector
    std::thread t2(read_only, std::cref(vec));    // 只读引用,不拷贝
    std::thread t3(process, x);                    // 安全地传值
    
    t1.join(); t2.join(); t3.join();
}

一句话记忆:std::thread 默认"防呆"------所有参数先拷贝一份保命;如果你确定要引用,必须用 std::ref 或 std::cref 显式声明,并自己保证原对象生命周期足够长。

1.10.7.答疑

c 复制代码
#include <thread>

void increment(int& x) {
    ++x;
}

int main() {
    int value = 0;
    std::thread t(increment, value);  // ❌ 编译错误!
    t.join();
}

解释上述编译中不匹配报错,正常下传递int变量给接受int&作为形参的函数应该可以?

对比:普通调用 vs std::thread 调用

场景 A:普通函数调用(✅ 编译通过)

c 复制代码
void increment(int& x) { ++x; }

int main() {
    int value = 0;
    increment(value);  // ✅ OK
}

发生了什么:

c 复制代码
increment(value)
       │
       ▼
   value 是具名变量(左值 lvalue)
       │
       ▼
   绑定到 int& x(非 const 左值引用)
       │
       ▼
   ✅ 合法:左值引用可以绑定到左值

场景 B:std::thread 调用(❌ 编译失败)

c 复制代码
std::thread t(increment, value);

发生了什么:

c 复制代码
// std::thread 构造函数模板(简化)
template<class F, class... Args>
thread(F&& f, Args&&... args) {
    // Step 1: 参数被 decay-copy
    // value 的类型被推导为 int(不是 int&)
    // 然后创建副本:int __tmp = value;
    
    // Step 2: 在新线程内部调用
    // increment(__tmp);
    //      │
    //      ▼
    //   __tmp 是一个临时副本(虽然它有名字,但在转发后表现为右值/可移动对象)
    //   更准确地说,内部用 std::forward<int>(__tmp) 转发
    //      │
    //      ▼
    //   试图绑定到 int& x(非 const 左值引用)
    //      │
    //      ▼
    //   ❌ 非法:非 const 左值引用不能绑定到右值/临时对象!
}

根本原因:std::thread 的"参数存储"层

std::thread 不是直接把你的参数递给函数,而是先拷贝一份存起来,等线程启动时再用这份副本去调用你的函数。

c 复制代码
普通调用:
value ──────► increment(int& x)
(左值)          直接绑定,OK

std::thread 调用:
value ──► [thread 内部存储] ──► increment(int& x)
(左值)       创建了副本 tmp      试图用 tmp 绑定到 int&
             (转发时成为右值)    ❌ 非 const 左值引用不能接右值!

形参为const int&则可以,因为此时实参可以是右值/可移动对象。

为什么 std::ref 能解决问题?

c 复制代码
std::thread t(increment, std::ref(value));

std::ref(value) 创建的是 std::reference_wrapper 对象:

  • 这个对象本身被值拷贝到线程内部(安全,它只是个轻量包装器)
  • 但它内部持有 value 的地址
  • 调用时,reference_wrapper 隐式转换为 int&,相当于 increment(value) 的原始语义被还原
c 复制代码
std::thread t(increment, std::ref(value));

内部存储:
    reference_wrapper<int> tmp(std::ref(value));
    // tmp 里存的是 &value

新线程调用时:
    increment(tmp);  // tmp 隐式转为 int&,即 value 本身
    // 相当于 increment(value) ------ 完美还原!
相关推荐
j7~2 小时前
【C++】智能指针的使用及其原理--详解
c++·c++11·内存泄漏·智能指针·raii·boost智能指针
Persistent的粽子!3 小时前
C++:类与对象(一)
开发语言·c++·经验分享·笔记
库玛西3 小时前
深入浅出Linux select网络模型:从底层位图原理到C++面向对象高级封装
linux·服务器·网络·c++·ubuntu
2402_882893864 小时前
深入浅出 unordered_map 与 unordered_set——从使用到底层差异
c++·哈希·unordered_map·unordered_set
今天要早睡_4 小时前
C++ 核心语法速过:命名空间、引用、函数重载与 nullptr 深度解析
android·java·c++
汉克老师5 小时前
CSP-J 初赛(以满分为目标):第二十七课 《图的遍历——BFS广度优先搜索——像“水波纹”一样,一层一层地搜索》
c++·csp-j·小学生·学c++编程
闻缺陷则喜何志丹5 小时前
【动态规划】P3609 [USACO17JAN] Hoof, Paper, Scissor G
c++·算法·动态规划·洛谷
汉克老师5 小时前
CSP-J 初赛(以满分为目标):第二十六课 《图的遍历——DFS深度优先搜索——从“树的先序遍历”走进真正的图世界》
c++·csp-j·小学生·学c++编程
程序猿编码5 小时前
基于GGML的C++17轻量化语音推理引擎:说话人识别与语音分析技术全解析
开发语言·c++·pytorch·深度学习·神经网络·大模型