《编程通识》之多线程

📅 2026/7/15 23:47:03 👁️ 阅读次数 📝 编程学习
《编程通识》之多线程

文章目录

  • 一、基础核心概念
    • 1.1 程序(Program)
    • 1.2 进程(Process)
    • 1.3 线程(Thread)
    • 1.4 串行(Serial)
    • 1.5 并行(Parallelism)
    • 1.6 并发(Concurrency)
  • 二、C++ 多线程优势
    • 2.1 提升运行效率,利用多核CPU
    • 2.2 提升交互体验 & 响应性
    • 2.3 模块化逻辑拆分
    • 2.4 补充代价
  • 三、线程安全相关核心概念
    • 3.1 线程安全
    • 3.2 临界区(Critical Section)
    • 3.3 同步(Synchronization)
    • 3.4 互斥(Mutual Exclusion)
    • 3.5 线程优先级
    • 3.6 死锁(Deadlock)
  • 四、线程安全问题常见原因 & 排查方法
    • 4.1 常见原因
    • 4.2 排查方法
  • 五、C++ 多线程基础代码示例(极简)

一、基础核心概念

1.1 程序(Program)

  • 存放在磁盘上的可执行静态文件.exe/可执行二进制文件、源码编译产物),是指令和数据的集合,本身不运行、不占用运行时内存。
  • 示例:a.exe、编译好的 C++ 可执行文件。

1.2 进程(Process)

  • 程序运行后的实例,操作系统资源分配的基本单位,拥有独立地址空间、堆、全局变量、文件句柄、CPU时间片等系统资源。
  • 进程之间相互隔离、默认不能直接访问彼此内存,进程切换开销很大。
  • 一个进程至少包含1 个主线程

1.3 线程(Thread)

  • 进程内部的执行分支/执行流,是操作系统CPU调度的基本单位,共享所属进程的地址空间、全局变量、堆、文件资源,拥有自己独立栈、寄存器、局部变量。
  • 线程切换开销远小于进程切换,轻量级执行单元。
  • C++11 引入标准库:std::thread

1.4 串行(Serial)

  • 依次执行任务,同一时刻只有一个任务在执行,完成一个再做下一个;
    • 优点:逻辑简单、天然线程安全、无竞争
    • 缺点:CPU多核资源浪费、整体耗时长

1.5 并行(Parallelism)

  • 同一时刻多个任务同时运行(依赖多核CPU/多处理器硬件),真正同时执行多条指令,物理上同步运行,用于把大任务分块同时计算,提升计算速度。
    • 例:多核CPU上多个线程各自跑在不同CPU核心

1.6 并发(Concurrency)

  • 宏观上同时推进多个任务,微观上交替执行(单核CPU靠操作系统时间片轮转调度快速切换线程,给人同时运行的错觉)
    • 并发 ≠ 并行;并行属于并发的子集
    • 核心目的:提升响应性、避免主线程阻塞(如UI程序)

二、C++ 多线程优势

2.1 提升运行效率,利用多核CPU

  • 把计算密集型任务拆分为多个子任务并行执行,充分利用多核处理器算力,缩短总运算时间;
  • 分离IO等待(文件读写、网络请求)和计算任务:IO阻塞时CPU线程可继续运算,避免CPU空等,提高整体吞吐量。

2.2 提升交互体验 & 响应性

  • 主线程(UI/交互线程)负责响应用户输入、刷新界面;子线程执行耗时运算/IO任务,防止主线程卡死、程序无响应;保证交互流畅。

2.3 模块化逻辑拆分

  • 按功能职责拆分线程(通信线程、计算线程、日志线程),简化业务逻辑架构;
  • 部分场景可提高整体系统稳定性(例如隔离后台任务异常,减少主线程崩溃概率)。

2.4 补充代价

  • 额外开销:线程创建/销毁、上下文切换、同步锁等待;
  • 增加代码复杂度,引入线程竞争、死锁等风险。

三、线程安全相关核心概念

3.1 线程安全

  • 定义:多线程同时访问共享数据(全局变量、静态变量、堆内存对象)时,程序行为结果始终正确、符合预期,不会出现数据错乱、脏读、异常崩溃。
  • 非线程安全根源多个线程同时读写同一可变共享资源(竞争资源 / 临界区),读写操作不是原子操作
    • ✅ 只读常量/const对象:天然线程安全
    • ❌ 可读写全局/静态变量、共享对象:默认非线程安全

3.2 临界区(Critical Section)

  • 访问共享可变资源的代码片段,同一时刻只允许一个线程进入执行,防止并发读写冲突。

3.3 同步(Synchronization)

  • 协调多个线程执行顺序、访问时机,保证共享资源访问有序、数据一致;
    • C++常用同步机制:
      • std::mutex:互斥锁
      • std::lock_guard/std::unique_lock:RAII锁管理
      • std::condition_variable:条件变量(线程等待/唤醒,生产者消费者模型)
      • std::atomic<T>:原子类型(无锁线程安全,适用于简单数值变量)
      • std::future/std::promise/std::async:异步任务同步

3.4 互斥(Mutual Exclusion)

  • 同步核心机制:保证同一时刻仅有一个线程进入临界区访问共享资源,实现互斥访问。
    • std::mutex:基础互斥锁
    • std::recursive_mutex:递归互斥锁(同一线程可多次加锁)
    • std::timed_mutex:超时互斥锁,避免永久阻塞

3.5 线程优先级

  • OS 调度参数,决定CPU分配时间片的优先程度,优先级高的线程更容易被CPU调度执行;
    • 风险:优先级反转(低优先级线程持有锁,高优先级线程持续等待)、饥饿问题;
    • C++标准本身不直接提供跨平台优先级API,通常依赖平台API(Windows/Linux系统调用)。

3.6 死锁(Deadlock)

✅ 死锁定义
多个线程互相持有对方需要的锁/资源,互相无限等待,所有线程都无法继续执行,程序卡死。

✅ 死锁四大必要条件(Coffman条件)

  1. 互斥条件:资源是独占访问(锁互斥)
  2. 持有并等待:线程持有已有锁,同时请求另外一把锁,不释放已有锁
  3. 不可剥夺条件:锁不能被操作系统强行回收
  4. 循环等待条件:线程形成环形等待依赖(A等B锁,B等C锁,C等A锁)

✅ 死锁解决/预防方法

  • 固定锁的加锁顺序(所有线程按相同顺序申请锁)
  • 使用统一加锁函数:std::lock(多个mutex)一次性加锁
  • 设置超时锁(std::timed_mutex),避免无限阻塞
  • 减少嵌套锁,减少全局共享锁,缩小临界区代码范围
  • 使用原子变量减少锁依赖

四、线程安全问题常见原因 & 排查方法

4.1 常见原因

  1. 共享可变数据竞争(数据竞争 Data Race)
    • 多线程无保护并发读写同一个非原子共享变量/对象,指令被CPU乱序执行、多核缓存不一致,造成脏数据、数值错乱、崩溃
    • 例如:全局计数变量int cnt,多线程直接cnt++(非原子操作,会拆分为读/改/写三步)
  2. 错误/不当使用锁
    • 漏加锁、加锁粒度不合理(锁太大性能差 / 锁太小没覆盖全部临界区)、忘记解锁、重复加锁
    • 锁嵌套不当,形成循环依赖,引发死锁
    • 混用同步机制,时序错乱
  3. 内存可见性 & CPU乱序优化问题
    • 编译器/CPU指令重排、CPU缓存(缓存行)不一致,一个线程修改的数据对其他线程不可见
    • 解决:std::atomic、内存屏障、volatile(慎用,C++不保证线程安全,仅影响编译器优化)
  4. 对象生命周期问题(野指针/悬垂引用)
    • 主线程提前析构共享对象,子线程继续访问已销毁对象,造成内存越界崩溃
    • 异步回调、lambda捕获引用错误(捕获局部变量引用,变量提前销毁)
  5. 虚假唤醒(Spurious Wakeup)
    • std::condition_variable条件变量被意外唤醒,未检查真实条件直接执行代码,引发错误
    • 正确写法:while(条件不满足) cv.wait(...),而非if
  6. 优先级调度问题、线程饥饿、异步回调时序错误

4.2 排查方法

  1. 代码审查
    • 检查全局/静态变量、共享对象访问逻辑;确认临界区范围和锁匹配;检查lambda捕获方式(值捕获vs引用捕获)
    • 检查锁嵌套顺序,排查循环等待风险
  2. 工具检测
    • 内存/并发检查工具:Valgrind、AddressSanitizer(ASAN)、ThreadSanitizer(TSan)(检测data race数据竞争)
    • 调试器(gdb/lldb/VS调试):断点查看线程状态、锁持有情况、调用栈,定位死锁/异常线程
    • 日志追踪:记录线程ID、锁获取/释放时机、变量值变化
  3. 复现与简化测试
    • 增加并发压力测试,复现偶发bug;逐步缩小临界区代码,定位冲突变量
    • 使用原子类型/无锁结构做对照验证;加锁/不加锁对照复现问题
  4. 增加断言/校验
    • 对共享数据增加状态校验、边界检查,提前捕获脏读/非法访问
    • 检查条件变量等待逻辑,用while循环防虚假唤醒
  5. 查看CPU/系统状态
    • 死锁时CPU占用率很低,线程卡在等待锁状态,可查看线程阻塞调用栈

五、C++ 多线程基础代码示例(极简)

#include<iostream>#include<thread>#include<mutex>std::mutex mtx;intcount=0;voidadd(){for(inti=0;i<10000;++i){std::lock_guard<std::mutex>lock(mtx);// RAII自动加锁/解锁count++;}}intmain(){std::threadt1(add);std::threadt2(add);t1.join();t2.join();std::cout<<count<<std::endl;return0;}