进程 vs 线程:从原理到区别,一次讲清楚

张开发
2026/6/9 15:57:20 15 分钟阅读
进程 vs 线程:从原理到区别,一次讲清楚
面试官“聊聊进程和线程的区别吧”大多数人都能把八股文背出来 “一个是资源分配单位一个是调度执行单位”。这问题背后藏的是操作系统资源调度的核心逻辑远没表面那么简单。如果把操作系统比作一座大型工厂进程就是工厂里独立的生产车间而线程则是车间里各司其职的工人。带着这个比喻往下看很多抽象概念你会瞬间清晰。Part1 进程的本质1.1、进程的 “激活” 过程当你双击打开一个软件、敲下指令运行一段程序时操作系统不会直接让程序跑起来它会先给这个程序划一块 “专属地盘”—— 也就是独立的地址空间。这块地盘被分成了三个核心区域代码区存放程序的执行指令相当于车间里的生产手册所有操作都得按手册来数据区存储全局变量、静态变量等公共数据好比车间里的公共物料架堆栈区栈区负责函数调用的临时数据堆区用于动态分配内存就像工人手边的临时工具台和可申请的备用物料箱。有了这块独立地盘程序才算真正拥有了 “运行资格”此时它就从静态的代码变成了动态的进程。我们可以给进程一个通俗定义进程是程序在专属地址空间内的动态执行实例它带着 OS 分配的独立资源是系统资源分配的基本单位。结合工厂类比进程有三个核心特征很好理解动态性与静态性的区别程序是写在硬盘里的 “生产手册”是静态的而进程是手册被拿到车间、工人按手册开工的过程是动态的程序运行则进程生程序停止则进程灭资源分配的独立单位每个车间进程都有自己的物料、工具和生产区域OS 会给每个车间独立分配资源CPU 资源除外车间之间的物料互不流通对应进程的地址空间相互隔离一个进程崩溃不会影响其他进程调度的基本单元非 CPU 层面OS 会统筹各个车间的开工顺序但不会直接管车间里工人的分工这也为后续线程的出现埋下了伏笔。1.2、进程上下文切换进程切换是操作系统的核心能力如从 “浏览器” 切到 “音乐软件”而实现 “无缝切换” 的关键就是进程上下文—— 它是进程运行状态的完整快照包含四类必须保存的信息上下文组成技术定义工厂类比车间切换CPU 寄存器值累加器存计算结果、程序计数器PC存下条指令地址、栈指针存栈顶位置等记录车间设备参数如机床转速、卡尺当前读数进程状态由 PCB 管理含 “就绪 / 运行 / 阻塞” 等状态如进程因等待网络请求进入 “阻塞” 态记录车间生产进度如 “待料中”“加工中”“已完工”地址空间信息页表 / 段表映射虚拟内存到物理内存、内存权限如代码区只读、数据区可读写记录车间布局如 “物料区在东、设备区在西” 区域规则如 “物料区禁止吸烟”内核栈与用户栈内容内核栈存系统调用参数如open函数的文件路径用户栈存函数局部变量 / 调用参数记录车间管理员的调度笔记内核栈、工人的操作记录用户栈进程上下文的作用为什么切换进程开销大以 “浏览器切到音乐软件” 为例操作系统会执行两步操作保存浏览器进程上下文将浏览器的寄存器值、页表、栈数据全部写入 PCB—— 相当于给浏览器车间拍 “全景照”连设备参数、物料位置都不放过加载音乐进程上下文从音乐进程的 PCB 中读取快照恢复寄存器值、重建内存映射 —— 相当于按 “全景照” 还原音乐车间继续之前的播放进度。正因为进程上下文包含 “地址空间信息” 这类重量级数据进程切换的开销通常是线程切换的 10~100 倍—— 这也是线程存在的核心原因。Part2 线程的由来与特性进程虽能实现 “独立运行”但切换开销太大。为解决 “轻量化执行” 需求线程Thread作为 “进程内的执行分支” 应运而生类比车间里的 “生产线”—— 共享车间资源仅保留专属执行工具。2.1、线程的本质线程不单独拥有资源而是 “复用” 所属进程的大部分资源仅保留三类 “执行必需的私有资源”确保独立运行运行栈存函数局部变量、调用参数如void func(int a)中的a每个线程有独立栈空间避免数据干扰程序计数器PC记录当前线程下一条指令地址确保切换后能 “续上” 执行部分寄存器如通用寄存器存临时计算结果、栈指针指向栈顶属于线程私有不与其他线程共享。这三类私有资源统称线程上下文—— 对比进程上下文它不含 “地址空间信息”因此切换时只需保存 / 加载少量数据开销极低。2.2、核心技术点线程共享的进程资源“线程共享进程资源” 是核心特性但具体共享哪些、如何验证下面分五类拆解1共享代码区所有线程可执行同一函数代码区存编译后的机器指令如thread_func函数同一进程内的线程可直接调用任意函数 —— 类比所有生产线共用一本 “设计图”无需单独复印。代码#include iostream #include thread using namespace std; // 代码区的函数t1、t2均可调用 void thread_func(int thread_id) { cout 线程 thread_id 执行代码区函数 endl; } int main() { thread t1(thread_func, 1); // 线程1调用thread_func thread t2(thread_func, 2); // 线程2调用同一函数 t1.join(); t2.join(); return 0; }输出线程1执行代码区函数 线程2执行代码区函数2共享数据区全局变量、静态变量多线程可见数据区存全局变量如int global_var和静态变量如static int static_var所有线程访问的是 “同一内存地址”—— 类比车间的 “公共物料架”一个生产线用了其他线看到的数量会减少。代码#include iostream #include thread #include mutex using namespace std; mutex mtx; // 互斥锁避免cout输出混乱 int global_var 10; // 数据区全局变量线程共享 void modify_global(int thread_id) { lock_guardmutex lock(mtx); global_var thread_id; // 线程1加1线程2加2 cout 线程 thread_id 修改后global_var global_var endl; } int main() { thread t1(modify_global, 1); thread t2(modify_global, 2); t1.join(); t2.join(); cout 主线程读取global_var global_var endl; // 主线程也能访问 return 0; }输出线程1修改后global_var11 线程2修改后global_var13 主线程读取global_var133共享堆区动态分配内存多线程可访问堆区是通过new/malloc动态分配的内存如int* p new int(10)只要线程持有指针就能读写该内存 —— 类比车间的 “备用物料库”所有生产线知道库位指针就能取用。代码#include iostream #include thread #include mutex using namespace std; mutex mtx; int* heap_var new int(10); // 堆区内存线程共享 void modify_heap(int thread_id) { lock_guardmutex lock(mtx); *heap_var thread_id * 5; // 线程1加5线程2加10 cout 线程 thread_id 修改后heap_var *heap_var endl; } int main() { thread t1(modify_heap, 1); thread t2(modify_heap, 2); t1.join(); t2.join(); delete heap_var; // 堆区需手动释放进程退出会自动回收 return 0; }输出线程1修改后heap_var15 线程2修改后heap_var254共享文件描述符打开的文件 / 网络连接多线程可用进程打开的文件如FILE* f fopen(log.txt, w)、网络连接如socket会被分配 “文件描述符”整数标识该描述符属于进程所有线程可通过它读写 —— 类比车间的 “公共打印机”所有生产线都能使用。场景示例#include iostream #include thread #include cstdio #include mutex using namespace std; mutex mtx; FILE* log_file fopen(app.log, w); // 进程打开的文件共享 void write_log(int thread_id) { lock_guardmutex lock(mtx); fprintf(log_file, 线程%d写入日志\n, thread_id); // 多线程共享文件描述符 } int main() { thread t1(write_log, 1); thread t2(write_log, 2); t1.join(); t2.join(); fclose(log_file); return 0; }查看*app.log内容线程1写入日志 线程2写入日志5共享信号处理方式进程注册的信号回调多线程生效进程通过signal函数注册的信号处理逻辑如处理CtrlC的SIGINT信号对所有线程生效 —— 类比车间的 “紧急停机规则”所有生产线听到警报后都会按同一规则停机。场景示例#include iostream #include thread #include signal.h #include unistd.h using namespace std; // 进程注册的信号处理函数所有线程生效 void sig_handler(int sig) { cout 收到信号 sig 所有线程停止运行 endl; exit(0); } void thread_task() { while (true) { sleep(1); cout 线程运行中... endl; } } int main() { signal(SIGINT, sig_handler); // 注册SIGINT信号CtrlC触发 thread t1(thread_task); thread t2(thread_task); t1.join(); t2.join(); return 0; }操作与输出运行程序线程持续输出 “线程运行中...”按CtrlC触发SIGINT信号所有线程停止输出 “收到信号 2所有线程停止运行”。Part3 关系和区别3.1、本质区别核心定位进程操作系统资源分配的基本单位给车间分地盘、物料、图纸线程处理器CPU任务调度和执行的基本单位给工人分配生产线。3.2、包含关系层级归属一个进程至少包含一个线程一个车间至少有一条生产线才能开工线程是进程的组成部分因开销远低于进程被称为 “轻权进程” 或 “轻量级进程”。3.3、资源开销成本差异进程每个进程有独立地址空间专属车间创建、销毁、切换时需处理全套资源地盘、物料、图纸开销大线程同一进程内的线程共享地址空间共用车间资源仅私有运行栈和程序计数器专属操作台和记录本创建、切换、销毁开销小。3.4、影响关系稳定性差异进程地址空间相互隔离车间独立一个进程崩溃后在保护模式下其他进程不受影响一个车间塌了其他车间正常生产线程共享进程资源共用车间物料一个线程崩溃可能导致整个进程被 OS 杀掉一条生产线出故障可能毁了整个车间的物料导致车间停工因此多进程比多线程更健壮。Part4 并发、并行与任务类型有了 “车间 - 生产线 - 工人” 模型咱们再拆解两个高频考点并发 vs 并行以及 CPU/IO 密集型任务。4.1、并发与并行单 CPU 如何 “同时” 多任务一个基本事实单核 CPU 在一个瞬间只能处理一个任务。但为什么我们用单核心电脑时能同时听音乐、浏览网页答案是 ——时间片轮转调度。OS 会给每个进程或线程分配一个 “时间片”比如 10ms即 CPU 每次执行该任务的最长时间。时间一到不管任务是否完成OS 都会强制把 CPU 切换到下一个任务。就像工人单核 CPU给每条生产线线程分配 10 分钟操作时间到点就换线 —— 虽然每个瞬间只操作一条线但切换速度极快CPU 每秒切换成千上万次人类完全感觉不到顿挫误以为是 “同时运行”。这就区分了两个概念并发单工人单核 CPU轮流操作多生产线线程靠快速切换实现 “看似同时”并行多工人多核 CPU同时操作多生产线线程实现 “真正同时”。比如一个厨师轮流炒两锅菜并发两个厨师各炒一锅并行—— 这也是面试中区分两者的核心要点。4.2、任务类型CPU 密集型 vs IO 密集型CPU 密集型任务如数据运算、算法执行建议用 “多进程” 或 “少线程”—— 避免频繁线程切换浪费时间单核下多线程反而变慢IO 密集型任务如读文件、网络请求建议用 “多线程”—— 线程等待 IO 时CPU 可切换到其他线程提升利用率如一个线程等网络响应时另一个线程处理本地逻辑。Part5 代码实操直观感受进程与线程的开销差异代码实现Linux 环境#include iostream #include thread #include unistd.h #include sys/wait.h #include chrono using namespace std; const int LOOP_NUM 10000; // 任务循环次数模拟切换频率 // 线程任务空循环仅用于消耗时间片 void thread_task() { for (int i 0; i LOOP_NUM; i); } // 进程任务空循环与线程任务逻辑一致 void process_task() { for (int i 0; i LOOP_NUM; i); exit(0); // 子进程执行完退出 } int main() { // 1. 统计线程切换耗时 auto start_thread chrono::high_resolution_clock::now(); thread t1(thread_task); thread t2(thread_task); t1.join(); t2.join(); auto end_thread chrono::high_resolution_clock::now(); auto dur_thread chrono::duration_castchrono::microseconds(end_thread - start_thread).count(); // 2. 统计进程切换耗时 auto start_process chrono::high_resolution_clock::now(); pid_t pid1 fork(); // 创建子进程1 if (pid1 0) process_task(); pid_t pid2 fork(); // 创建子进程2 if (pid2 0) process_task(); waitpid(pid1, nullptr, 0); // 等待子进程1退出 waitpid(pid2, nullptr, 0); // 等待子进程2退出 auto end_process chrono::high_resolution_clock::now(); auto dur_process/doubaocanvas

更多文章