《编程通识》之多线程
📅 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:异步任务同步
- C++常用同步机制:
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条件)
- 互斥条件:资源是独占访问(锁互斥)
- 持有并等待:线程持有已有锁,同时请求另外一把锁,不释放已有锁
- 不可剥夺条件:锁不能被操作系统强行回收
- 循环等待条件:线程形成环形等待依赖(A等B锁,B等C锁,C等A锁)
✅ 死锁解决/预防方法
- 固定锁的加锁顺序(所有线程按相同顺序申请锁)
- 使用统一加锁函数:
std::lock(多个mutex)一次性加锁 - 设置超时锁(
std::timed_mutex),避免无限阻塞 - 减少嵌套锁,减少全局共享锁,缩小临界区代码范围
- 使用原子变量减少锁依赖
四、线程安全问题常见原因 & 排查方法
4.1 常见原因
- 共享可变数据竞争(数据竞争 Data Race)
- 多线程无保护并发读写同一个非原子共享变量/对象,指令被CPU乱序执行、多核缓存不一致,造成脏数据、数值错乱、崩溃
- 例如:全局计数变量
int cnt,多线程直接cnt++(非原子操作,会拆分为读/改/写三步)
- 错误/不当使用锁
- 漏加锁、加锁粒度不合理(锁太大性能差 / 锁太小没覆盖全部临界区)、忘记解锁、重复加锁
- 锁嵌套不当,形成循环依赖,引发死锁
- 混用同步机制,时序错乱
- 内存可见性 & CPU乱序优化问题
- 编译器/CPU指令重排、CPU缓存(缓存行)不一致,一个线程修改的数据对其他线程不可见
- 解决:
std::atomic、内存屏障、volatile(慎用,C++不保证线程安全,仅影响编译器优化)
- 对象生命周期问题(野指针/悬垂引用)
- 主线程提前析构共享对象,子线程继续访问已销毁对象,造成内存越界崩溃
- 异步回调、lambda捕获引用错误(捕获局部变量引用,变量提前销毁)
- 虚假唤醒(Spurious Wakeup)
std::condition_variable条件变量被意外唤醒,未检查真实条件直接执行代码,引发错误- 正确写法:
while(条件不满足) cv.wait(...),而非if
- 优先级调度问题、线程饥饿、异步回调时序错误
4.2 排查方法
- 代码审查
- 检查全局/静态变量、共享对象访问逻辑;确认临界区范围和锁匹配;检查lambda捕获方式(值捕获vs引用捕获)
- 检查锁嵌套顺序,排查循环等待风险
- 工具检测
- 内存/并发检查工具:Valgrind、AddressSanitizer(ASAN)、ThreadSanitizer(TSan)(检测data race数据竞争)
- 调试器(gdb/lldb/VS调试):断点查看线程状态、锁持有情况、调用栈,定位死锁/异常线程
- 日志追踪:记录线程ID、锁获取/释放时机、变量值变化
- 复现与简化测试
- 增加并发压力测试,复现偶发bug;逐步缩小临界区代码,定位冲突变量
- 使用原子类型/无锁结构做对照验证;加锁/不加锁对照复现问题
- 增加断言/校验
- 对共享数据增加状态校验、边界检查,提前捕获脏读/非法访问
- 检查条件变量等待逻辑,用while循环防虚假唤醒
- 查看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;}
编程学习
技术分享
实战经验