[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 转移所有权。
相关推荐
Mr_WangAndy1 小时前
C++17 新特性_第二章 C++17 语言特性_std::any和string_view
c++·string_view·c++40周年·c++17新特性·c++新特性any
水天需0103 小时前
C++ 三种指针转换深度解析
c++
言言的底层世界4 小时前
c++中STL容器及算法等
开发语言·c++·经验分享·笔记
Mr_WangAndy4 小时前
C++17 新特性_第一章 C++17 语言特性___has_include,u8字符字面量
c++·c++40周年·c++17新特性·__has_include·u8字面量
liu****4 小时前
八.函数递归
c语言·开发语言·数据结构·c++·算法
Vanranrr4 小时前
C++临时对象与悬空指针:一个导致资源加载失败的隐藏陷阱
服务器·c++·算法
BestOrNothing_20155 小时前
【C++基础】Day 5:struct 与 class
c++·c·class类·struct结构体·typename模板·private与public
枫叶丹45 小时前
【Qt开发】Qt窗口(三) -> QStatusBar状态栏
c语言·开发语言·数据库·c++·qt·microsoft
Skrrapper5 小时前
【编程史】微软的起家之路:一代传奇的诞生
数据库·c++·microsoft