[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 转移所有权。
相关推荐
Q741_14713 小时前
每日一题 力扣 3655. 区间乘法查询后的异或 II 模拟 分治 乘法差分法 快速幂 C++ 题解
c++·算法·leetcode·模拟·快速幂·分治·差分法
夏乌_Wx13 小时前
剑指offer | 2.4数据结构相关题目
数据结构·c++·算法·剑指offer·c/c++
米啦啦.13 小时前
C+类的友元与静态成员函数,类模板
c++·友元·类模板
超绝振刀怪13 小时前
【C++可变模板参数】
开发语言·c++·可变模板参数
minji...14 小时前
Linux 线程同步与互斥(二) 线程同步,条件变量,pthread_cond_init/wait/signal/broadcast
linux·运维·开发语言·jvm·数据结构·c++
梓䈑14 小时前
高性能 C++ 日志实战:spdlog 核心架构解析与最佳实践指南
c++·架构
草莓熊Lotso14 小时前
【Linux 线程进阶】进程 vs 线程资源划分 + 线程控制全详解
java·linux·运维·服务器·数据库·c++·mysql
唐樽14 小时前
C++ 竞赛学习路线笔记
c++·笔记·学习
ShineWinsu14 小时前
对于Linux:文件操作以及文件IO的解析
linux·c++·面试·笔试·io·shell·文件操作
十五年专注C++开发15 小时前
Oat++: 一个轻量级、高性能、零依赖的 C++ Web 框架
开发语言·c++·web服务·oatpp