[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 转移所有权。
相关推荐
ULTRA??7 小时前
基于range的函数式编程C++,python比较
c++·python·kotlin·c++20
闻缺陷则喜何志丹7 小时前
【计算几何 二分查找】P5485 [JLOI2010] 铁人双项比赛|普及+
c++·数学·二分查找·计算几何·洛谷
..空空的人7 小时前
C++基于protobuf实现仿RabbitMQ消息队列---服务器模块认识1
服务器·开发语言·c++·分布式·rabbitmq·protobuf
晨非辰7 小时前
基于Win32 API控制台的贪吃蛇游戏:从设计到C语言实现详解
c语言·c++·人工智能·后端·python·深度学习·游戏
小此方7 小时前
Re: ゼロから学ぶ C++ 入門(六)类和对象·第三篇:运算符重载
开发语言·c++·后端
2301_789015627 小时前
每日精讲:环形链表、两个数组中的交集、随机链表的复制
c语言·数据结构·c++·算法·leetcode·链表·排序算法
2301_789015628 小时前
C++:二叉搜索树
c语言·开发语言·数据结构·c++·算法·排序算法
leiming616 小时前
C++ vector容器
开发语言·c++·算法
apocelipes18 小时前
从源码角度解析C++20新特性如何简化线程超时取消
c++·性能优化·golang·并发·c++20·linux编程
ozyzo18 小时前
求1~n的累加和
c++