C++ 笔记 thread

C++11 之前,多线程编程依赖平台特定 API(如 POSIX 的 pthread 或 Windows API),跨平台性差且管理繁琐。C++11 引入的 <thread> 库彻底改变了这一现状,它基于 RAII 机制 ,提供了跨平台的线程抽象,核心是将线程生命周期与对象生命周期绑定,同时配合同步原语(如 mutex)解决数据竞争问题。本文将从基础用法、生命周期管理、参数传递、同步配合到常见陷阱,系统拆解 std::thread 的核心知识点。


一、std::thread 基础:创建与启动线程

1.1 核心特性

std::thread 是线程的 "句柄类",对象创建后立即启动线程 (无需显式 start()),支持传入任意可调用对象(普通函数、lambda、函数对象、成员函数等)作为线程入口。

1.2 基本用法(带详细注释)

cpp 复制代码
#include <iostream>
#include <thread> // 线程库头文件
#include <string>

// 1. 普通函数作为线程入口
void print_message(const std::string& msg) {
    std::cout << "[线程] 消息: " << msg << "\n";
}

// 2. 函数对象(仿函数)作为线程入口
class Counter {
public:
    void operator()(int n) const {
        for (int i = 0; i < n; ++i) {
            std::cout << "[线程] 计数: " << i << "\n";
        }
    }
};

int main() {
    std::cout << "[主线程] 开始\n";

    // 方式1:用普通函数创建线程
    std::thread t1(print_message, "Hello, std::thread!");

    // 方式2:用 lambda 表达式创建线程(最灵活)
    std::thread t2([]() {
        std::cout << "[线程] Lambda 执行\n";
    });

    // 方式3:用函数对象创建线程
    Counter counter;
    std::thread t3(counter, 3); // 传入函数对象和参数

    // 注意:线程创建后立即执行,需用 join() 等待(后续详解)
    t1.join();
    t2.join();
    t3.join();

    std::cout << "[主线程] 结束\n";
    return 0;
}

二、线程生命周期管理:join () 与 detach ()

2.1 核心规则

std::thread 对象析构前,必须明确指定线程的 "归宿" :要么用 join() 等待线程执行完毕,要么用 detach() 让线程在后台独立运行。若析构时对象仍处于 joinable() 状态(未 join 且未 detach),程序会直接调用 std::terminate() 崩溃 ------ 这是 std::thread 最容易踩的陷阱。

2.2 关键函数与用法

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono> // 用于模拟耗时操作

void long_task() {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟2秒耗时
    std::cout << "[线程] 耗时任务完成\n";
}

int main() {
    // 1. join():等待线程执行完毕,回收线程资源
    {
        std::thread t(long_task);
        std::cout << "[主线程] 等待线程完成...\n";
        t.join(); // 阻塞主线程,直到 t 执行完毕
        std::cout << "[主线程] 线程已 join,资源回收\n";
        // t 析构时已不 joinable,安全
    }

    // 2. detach():分离线程,线程在后台独立运行,不再受 thread 对象控制
    {
        std::thread t(long_task);
        std::cout << "[主线程] 分离线程,让其后台运行\n";
        t.detach(); // 分离后,t 不再 joinable
        // 注意:detach 后线程可能访问已销毁的变量(如局部变量),需谨慎
    }

    // 3. joinable():判断线程是否可 join(未 join 且未 detach)
    std::thread t(long_task);
    std::cout << "[主线程] t 是否 joinable? " << (t.joinable() ? "是" : "否") << "\n";
    t.join();
    std::cout << "[主线程] join 后 t 是否 joinable? " << (t.joinable() ? "是" : "否") << "\n";

    // 陷阱演示:未 join/detach 直接析构(会崩溃!)
    // {
    //     std::thread t(long_task);
    // } // t 析构时仍 joinable,程序 terminate

    return 0;
}

三、线程参数传递:细节与陷阱

std::thread 的参数默认拷贝到线程栈(值传递),若需传递引用或不可拷贝对象,需特殊处理 ------ 这是最容易出错的地方。

3.1 完整用法示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <string>
#include <memory> // 用于 unique_ptr

// 1. 值传递:默认行为,安全但有拷贝开销
void func_by_value(int x) {
    x += 10;
    std::cout << "[线程] 值传递,x = " << x << "\n";
}

// 2. 引用传递:需用 std::ref()/std::cref(),否则会被当作值拷贝
void func_by_ref(int& x) {
    x += 10;
    std::cout << "[线程] 引用传递,x = " << x << "\n";
}

// 3. 移动语义:传递不可拷贝对象(如 unique_ptr),需用 std::move()
void func_by_move(std::unique_ptr<int> ptr) {
    std::cout << "[线程] 移动传递,*ptr = " << *ptr << "\n";
}

// 4. 成员函数作为线程入口:需传入对象指针/引用 + 成员函数地址
class Worker {
public:
    void do_work(const std::string& task) {
        std::cout << "[线程] 成员函数执行任务: " << task << "\n";
    }
};

int main() {
    int a = 10;
    std::cout << "[主线程] 初始 a = " << a << "\n";

    // 方式1:值传递(默认)
    std::thread t1(func_by_value, a);
    t1.join();
    std::cout << "[主线程] 值传递后 a = " << a << "\n"; // a 仍为 10(未修改)

    // 方式2:引用传递(必须用 std::ref()!)
    std::thread t2(func_by_ref, std::ref(a));
    t2.join();
    std::cout << "[主线程] 引用传递后 a = " << a << "\n"; // a 变为 20(已修改)

    // 错误示例:不用 std::ref(),会编译报错(或被当作值拷贝,取决于编译器)
    // std::thread t_error(func_by_ref, a); 

    // 方式3:移动语义(传递不可拷贝对象)
    auto ptr = std::make_unique<int>(100);
    std::thread t3(func_by_move, std::move(ptr));
    t3.join();
    // 注意:move 后 ptr 变为空,不能再访问
    if (!ptr) std::cout << "[主线程] ptr 已失去所有权\n";

    // 方式4:成员函数作为线程入口
    Worker worker;
    std::thread t4(&Worker::do_work, &worker, "处理数据"); // 传入成员函数地址、对象指针、参数
    t4.join();

    return 0;
}

四、std::thread 的移动语义与禁止拷贝

4.1 核心特性

std::thread 禁止拷贝 (删除了拷贝构造和拷贝赋值运算符),但支持移动语义 ------ 可以通过 std::move() 转移线程所有权,原对象不再 joinable。这使得 std::thread 可以存入容器(如 std::vector),但必须用移动操作。

4.2 用法示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

void task(int id) {
    std::cout << "[线程 " << id << "] 执行\n";
}

int main() {
    // 1. 移动构造:转移线程所有权
    std::thread t1(task, 1);
    std::thread t2 = std::move(t1); // t1 的所有权转移给 t2,t1 不再 joinable
    if (!t1.joinable()) std::cout << "[主线程] t1 已失去所有权\n";
    t2.join();

    // 2. 用容器存储 thread(必须用移动)
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(task, i + 1); // 直接在容器中构造 thread
        // 或:threads.push_back(std::thread(task, i + 1)); // 移动构造
    }
    // 遍历 join 所有线程
    for (auto& t : threads) {
        if (t.joinable()) t.join();
    }

    return 0;
}

五、线程同步基础:配合 mutex 解决数据竞争

std::thread 本身不处理同步,多线程共享数据时会出现数据竞争 (Data Race),导致未定义行为。需配合 <mutex> 库的互斥锁(std::mutex)和 RAII 包装器(std::lock_guardstd::unique_lock)解决。

5.1 完整示例:数据竞争与解决

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁头文件
#include <vector>

// 未同步的计数器:会出现数据竞争
class UnsafeCounter {
public:
    int count = 0;
    void increment() { count++; } // 非原子操作,多线程下会出错
};

// 同步的计数器:用 mutex 保护共享数据
class SafeCounter {
public:
    int count = 0;
    std::mutex mtx; // 互斥锁

    void increment() {
        // 用 std::lock_guard 自动管理锁:构造时 lock,析构时 unlock(RAII)
        std::lock_guard<std::mutex> lock(mtx);
        count++; // 临界区:同一时间只有一个线程能执行
    }
};

void test_unsafe() {
    UnsafeCounter counter;
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 1000; ++j) {
                counter.increment();
            }
        });
    }
    for (auto& t : threads) t.join();
    std::cout << "[未同步] 最终 count = " << counter.count << "(期望 10000,实际可能更小)\n";
}

void test_safe() {
    SafeCounter counter;
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 1000; ++j) {
                counter.increment();
            }
        });
    }
    for (auto& t : threads) t.join();
    std::cout << "[同步] 最终 count = " << counter.count << "(期望 10000,实际一致)\n";
}

int main() {
    test_unsafe();
    test_safe();
    return 0;
}

六、常见陷阱与最佳实践

6.1 核心陷阱

  1. 忘记 join/detachstd::thread 析构时若仍 joinable,程序直接崩溃 ------ 务必在析构前明确处理。
  2. 引用传递不用 std::ref():参数会被当作值拷贝,导致修改不生效或编译报错。
  3. 分离线程访问已销毁变量detach() 后线程若访问局部变量(已随作用域结束销毁),会触发未定义行为。
  4. 未保护共享数据 :多线程共享数据时必须用同步机制(mutex、原子变量等),否则会出现数据竞争。

6.2 最佳实践

优先用 join() :除非必须让线程后台运行,否则优先用 join() 明确等待线程结束,避免生命周期问题。

用 RAII 管理锁 :避免手动 lock()/unlock(),优先用 std::lock_guard(简单场景)或 std::unique_lock(灵活场景)。

避免共享数据:尽量让线程拥有独立数据,减少同步需求;若必须共享,明确用同步机制保护。

std::jthread(C++20) :C++20 引入的 std::jthread 自动在析构时 join(),更安全,若支持 C++20 优先使用。


相关推荐
南境十里·墨染春水2 小时前
C++ 笔记 高级线程同步原语与线程池实现
java·开发语言·c++·笔记·学习
lkforce2 小时前
MiniMind学习笔记(二)--model_minimind.py
笔记·python·学习·minimind·minimindconfig
瞎折腾啥啊2 小时前
CMake FetchContent与ExternalProject
c++·cmake·cmakelists
阿巴斯甜2 小时前
Predicate的使用:
java
阿巴斯甜2 小时前
Supplier的使用:
java
阿巴斯甜2 小时前
Function 用法:
java
三品吉他手会点灯3 小时前
STM32 VSCode 开发-C/C++的环境配置中,找不到C/C++: Edit Configurations选项
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
来自远方的老作者3 小时前
第10章 面向对象-10.4 继承
开发语言·python·继承·单继承·多继承·super函数
做个文艺程序员3 小时前
流式输出(SSE)在 Spring Boot 中的实现【OpenClAW + Spring Boot 系列 第3篇】
java·spring boot·后端