[C++高频精进] 并发编程:线程基础

核心要点速览

  • 线程 vs 进程:进程是资源分配单位(独立内存),线程是调度单位(共享进程内存),线程通信成本更低
  • 线程创建:std::thread支持函数、Lambda、函数对象三种方式
  • 线程管理:join()等待回收、detach()分离(慎用)、joinable()检查状态
  • 线程标识:std::this_thread::get_id()获取 ID,std::thread::id判断唯一性
  • 线程状态:就绪、运行、阻塞、终止
  • 常用接口: std::this_thread 命名空间提供 sleep_forsleep_untilyield 等工具函数。

一、线程的概念

1. 进程与线程的区别

特性 进程 (Process) 线程 (Thread)
定义 程序的一次执行实例,资源分配的最小单位。 进程内的执行单元,调度的最小单位。
资源 独立的代码、数据、堆栈空间。 共享进程的代码、全局数据,独立的栈和寄存器。
隔离 地址空间独立 (高隔离性)。 共享进程地址空间 (低隔离性)。
通信 进程间通信 (IPC),开销大 直接共享数据,开销小
开销 创建、切换、销毁开销大 创建、切换、销毁开销小 (轻量化)。

2. 线程的优势

  • 并发执行多个任务,提升程序响应速度。
  • 充分利用多核 CPU 资源,提高 CPU 利用率。
  • 比进程更轻量,资源消耗少、调度效率高。

3. 用户线程 vs 内核线程

  • 用户线程:用户空间管理,不依赖内核,创建销毁快,但内核无法感知,调度需用户实现。
  • 内核线程:内核空间管理,内核直接调度,支持真正并行,但创建销毁开销比用户线程高。
  • 常见映射:1:1(内核线程对应用户线程)、M:N(多个用户线程映射到多个内核线程)。

二、线程创建(std::thread)

C++11 std::thread标准化线程操作,跨平台兼容,无需依赖平台 API。

创建方式

  • 函数 / 函数指针:
cpp 复制代码
    void func(int a) { /* 线程逻辑 */ }
    std::thread t(func, 10);
  • Lambda 表达式: 简洁高效,可捕获外部变量(需注意捕获权限与生命周期)。
cpp 复制代码
    std::thread t([]{ 
        std::cout << "Lambda 线程" << std::endl; 
    });
  • 类成员函数: 需传递成员函数指针、对象指针(或引用)及参数。
cpp 复制代码
    class Task {public:
        void run(int a) { /* 线程逻辑 */ }};
    Task task;
    std::thread t(&Task::run, &task, 20); // &task 为对象指针

注意事项

  • 线程创建后需立即管理(join()detach()),否则析构时抛出std::terminate异常。
  • 传递参数时,默认按值拷贝,需传递引用时用std::ref/std::cref(避免拷贝开销或悬垂引用)。

三、线程生命周期与管理

1. 线程状态

  • 就绪:已创建,等待 CPU 调度(具备运行条件)。
  • 运行:占用 CPU,执行线程逻辑。
  • 阻塞:因等待资源(如锁、IO)暂停执行,释放 CPU。
  • 终止:线程执行完毕或被强制终止,资源等待回收。

2. 线程管理函数

(1)join()
  • 功能:主线程阻塞,等待子线程执行完毕后再继续,回收子线程资源(避免 "僵尸线程")。
  • 限制 :一个线程只能调用一次join(),调用后joinable()返回false
(2)detach()
  • 功能:主线程与子线程分离,子线程后台运行,主线程不等待。
  • 风险:子线程依赖的主线程资源(如局部变量)可能提前释放,导致悬垂引用(崩溃风险)。
  • 适用场景:子线程逻辑独立,不依赖主线程局部资源,且无需主线程等待。
(3)joinable()
  • 功能 :检查线程是否可join(未调用join()/detach(),且线程未终止)。
  • 用途 :避免重复joindetach导致的未定义行为(如join()已调用的线程)。

3. 僵尸线程与孤儿线程

  • 僵尸线程 (Zombie): 子线程已终止,但主线程未调用 join() 回收其进程控制块 (PCB) 资源,导致资源泄漏。

    • 危害: 长期积累会耗尽系统资源。
    • 避免: 必须 join()detach()
  • 孤儿线程 (Orphan): 主线程先于子线程退出,子线程被操作系统托管init 进程 (Linux) 或系统进程 (Windows),由托管进程负责回收。

    • 特点: 不会导致资源泄漏,但其执行逻辑必须是独立的。

4. std::this_thread 常用接口

std::this_thread 命名空间用于操作当前正在执行的线程:

接口 功能 示例
sleep_for(d) 让当前线程休眠指定时长,释放 CPU。 sleep_for(chrono::seconds(1));
sleep_until(tp) 让当前线程休眠到指定时间点。 适用于定时任务。
yield() 当前线程主动让出 CPU 给同优先级的就绪线程,自身回到就绪态。 适用于提升公平性,避免长时间占用 CPU。
get_id() 获取当前线程的唯一 ID。 cout << this_thread::get_id();

四、线程标识

  • 线程 IDstd::thread::id类型,每个线程有唯一标识(可通过==/!=判断唯一性)。
  • 获取方式
    • 子线程 ID:std::thread t(func); t.get_id();
    • 当前线程 ID:std::this_thread::get_id();
  • 特殊 ID:默认构造的std::thread::id表示 "无关联线程"(可判断线程是否有效)。
cpp 复制代码
    std::thread::id default_id; // "无关联线程" ID
    if (t.get_id() != default_id) { 
        // 线程 t 是有效线程
    }

五、易错

  1. 未管理std::thread:创建后未调用join()/detach(),析构时抛异常。
  2. detach()后访问主线程局部资源:子线程可能在主线程局部变量销毁后执行,导致悬垂引用。
  3. 重复join():对已join的线程再次调用join(),引发未定义行为(需用joinable()检查)。
  4. 线程参数按值传递:需传递引用时未用std::ref,导致拷贝开销或修改无效。
  5. 线程对象不允许直接赋值或拷贝(编译报错),必须使用 std::move 转移所有权。
相关推荐
小小龙学IT10 分钟前
oneTBB 开源并行编程库深度解析:从工作窃取调度器到 flow graph 实战
c++·开源
ShineWinsu1 小时前
对于C++:缓冲区管理的详细解析
linux·c++·面试·编程·笔试·io·缓冲区
h_a_o777oah1 小时前
【算法基础】卡特兰数:递推定义与折线对称公式推导及解题策略
c++·算法·acm·组合数学·卡特兰数·反射原理·动态规划dp
帅气的小峰2 小时前
pybind11与nanobind可以共存吗?如何迁移?
c++·python·cuda·推理引擎
syker2 小时前
SuperNova Compiler v4.2.9 完整开发手册
c++
zhy295632 小时前
在 QAIRT Genie SDK 上部署 Llama 3.2 1B 模型:从环境准备到 C++ 推理
开发语言·c++·llama
richard_yuu2 小时前
JSON vs XML vs 二进制:3种序列化终极选型
xml·c++·qt·学习·json
郝学胜-神的一滴3 小时前
并查集深度入门:从玄学抽象到 QuickFind & QuickUnion 源码实战
数据结构·c++·python·程序人生·算法·软件开发
ShineWinsu11 小时前
对于C++:IO流状态的详细解析
c++·面试·io·cin·io流状态·goodbit·failbit
Lzg_na13 小时前
constexpr的优势
c++