Skip to content

进程与线程

在操作系统和软件工程领域,进程 (Process)线程 (Thread) 是最基础也是最重要的两个概念。

1. 核心概念解析

1.1 进程 (Process)

  • 定义:进程是计算机中已运行程序的实体,是操作系统进行资源分配和调度的基本单位
  • 通俗理解:你可以把进程想象成一个“工厂”。当你双击打开一个微信(或 Chrome 浏览器、Word 文档)时,操作系统就为你启动了一个进程。
  • 组成部分:每个进程都有自己独立的一块地盘(内存空间)。这个空间里存放着:
    • 代码段:程序编译后的机器指令。
    • 数据段:全局变量和静态变量。
    • 堆 (Heap):程序运行时动态分配的内存。
    • 栈 (Stack):函数调用的局部变量和返回地址。
    • 文件描述符 / 打开的文件列表等系统资源。
  • 独立性:进程与进程之间是相互隔离的。微信的进程崩溃了,绝对不会影响到正在播放音乐的网易云进程。这是操作系统为了安全做出的保护机制。

1.2 线程 (Thread)

  • 定义:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
  • 通俗理解:你可以把线程想象成工厂里的“流水线工人”。一个工厂(进程)里至少有一个工人(主线程),也可以有多个工人(多线程)同时干活。
  • 资源共享:同一个进程内的所有线程,共享该进程的全部系统资源(如代码、数据、堆、打开的文件)。
  • 独立性:虽然共享资源,但每个线程拥有自己独立的:
    • 程序计数器 (PC):记录当前线程执行到哪一行代码了。
    • 一组寄存器:保存线程运行时的临时数据。
    • 线程栈 (Stack):保存每个线程自己函数调用的局部变量。
  • 轻量级:因为线程不需要拥有独立的内存空间(借用进程的),所以线程的创建、销毁和切换代价,远比进程小得多。因此线程被称为“轻量级进程”。

2. 进程与线程的详细对比

这是一张非常经典且必须掌握的对比表:

维度进程 (Process)线程 (Thread)
根本属性资源分配的最小单位。CPU 调度和执行的最小单位。
包含关系一个进程至少包含一个线程(主线程)。一个线程必定属于某个进程。
内存与资源每个进程拥有独立的地址空间和系统资源。同一进程下的线程共享该进程的地址空间和资源。但每个线程有独立的栈和寄存器。
开销 (Overhead)非常大。创建、销毁进程需要操作系统分配/回收内存等资源。很小。创建、销毁线程只需分配少量的栈空间。
上下文切换。需要切换页表(虚拟内存映射),刷新 TLB 缓存,开销极大。。不需要切换虚拟内存空间,只切换少量的寄存器和栈指针。
通信机制 (IPC)困难且复杂。需要操作系统的介入(如管道、消息队列、共享内存、信号量、Socket)。非常容易。由于共享内存空间,线程之间可以直接通过读写全局变量通信(但需要加锁同步)。
安全性/健壮性极高。一个进程崩溃通常不会影响其他进程(隔离机制)。较差。一个线程崩溃(如引发段错误段溢出),往往会导致整个进程随之崩溃。

3. 什么时候用多进程?什么时候用多线程?

  • 多进程适用场景

    • 需要极高稳定性和安全性的应用:典型的例子是现代浏览器(如 Chrome)。每一个标签页(Tab)或插件都在一个独立的进程中运行。如果某个网页崩溃了,只会关掉那个标签页,其他网页和整个浏览器都不受影响。如果用多线程,一个恶意的 JavaScript 代码就能搞垮整个浏览器。
    • CPU 密集型的任务(特别是 Python):由于 Python 中存在 GIL (Global Interpreter Lock,全局解释器锁) 的限制,Python 的多线程在处理计算密集型任务时无法真正利用多核 CPU(依然是串行执行)。此时,使用多进程 (Multiprocessing) 是唯一的解药,因为每个进程都有自己独立的 GIL。
  • 多线程适用场景

    • I/O 密集型任务:典型的如 Web 服务器处理并发请求(如下载文件、数据库查询、网络通信)。因为这些操作大部分时间都在等待数据返回(阻塞),在此期间 CPU 是空闲的。多线程可以充分利用这些等待时间,让其他线程继续处理别的请求。
    • 需要频繁共享大量数据的任务:因为线程间共享内存空间,所以通信成本极低,不需要像进程那样通过复杂的机制来拷贝数据。

4. 常见问题 (FAQ) 与面试题

4.1 什么是线程安全?如何保证线程安全?

  • 概念:当多个线程同时访问同一个共享资源(如修改同一个全局变量)时,如果不加任何控制,就会产生数据错乱或不可预期的结果。如果一段代码在多线程环境下运行,其结果和单线程环境完全一致,这就是线程安全的。
  • 保证方式
    • 互斥锁 (Mutex) / 同步机制:当一个线程在修改数据时,把资源锁住,其他线程必须排队等待。这是最常用的方法(如 Java 中的 synchronized 关键字或 ReentrantLock)。
    • 使用线程局部变量 (ThreadLocal):每个线程在本地维护该变量的副本,互不干扰,从而避免共享(空间换时间的策略)。
    • 无锁编程 (Lock-Free):利用底层的 CAS (Compare-And-Swap) 原子操作。

4.2 什么是死锁 (Deadlock)?产生的条件是什么?如何避免?

  • 概念:两个或多个线程(或进程)互相持有对方需要的锁,并在等待对方释放锁。结果大家都卡死在那里,谁也无法继续执行。
    • 比喻:A 拿着钥匙,想要开门;B 守在门边,想要拿钥匙。两人都不肯松手,于是僵持不下。
  • 产生死锁的四个必要条件(缺一不可):
    1. 互斥条件:一个资源每次只能被一个线程使用。
    2. 请求与保持条件:一个线程因请求被占用的资源而阻塞时,对自己已经获得的资源保持不放。
    3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺。
    4. 循环等待条件:多个线程之间形成一种头尾相接的循环等待资源的关系。
  • 如何避免/预防死锁(核心是破坏上面四个条件之一):
    • 破坏互斥条件:不可行,锁的意义就是互斥。
    • 破坏请求与保持:规定线程在开始执行前,必须一次性申请它需要的所有资源。如果申请不到,就什么也不拿,干等着。(缺点:资源利用率极低)。
    • 破坏不剥夺条件:如果一个已经持有部分资源的线程去申请新资源被拒绝,它必须主动释放自己当前持有的所有资源,过一会再重新尝试。(缺点:实现复杂,且可能导致线程反复尝试而做无用功,即“活锁”)。
    • 破坏循环等待条件(最实用):对系统中所有的资源进行编号,规定所有线程必须严格按照编号的递增顺序来申请锁。这样就永远不会出现循环等待的情况。

4.3 什么是协程 (Coroutine)?它和线程的区别是什么?

协程是一个更加轻量级的概念,有时被称为“用户态的线程”。

  • 调度者不同
    • 线程是由操作系统内核 (OS Kernel) 进行调度和管理的,切换成本较高。
    • 协程是由程序员在应用代码层面 (User Space) 通过程序库(如 Python 的 asyncio、Go 的 goroutine)自行控制调度的。操作系统根本不知道协程的存在。
  • 切换开销极小:因为协程的切换完全在用户态进行,不需要陷入内核态,不需要保存和恢复复杂的寄存器状态,只需要保存极少的上下文信息。所以一台机器上可以轻松创建几十万甚至上百万个协程(如 Golang),而线程通常只能创建几千个。
  • 并发模型:线程是抢占式的并发(操作系统随时可以中断你),协程通常是协作式的并发(协程主动让出执行权 yield/await 给别的协程)。

4.4 上下文切换 (Context Switch) 到底在切换什么?为什么进程切换比线程切换慢得多?

  • 上下文切换在切换什么:当 CPU 从执行任务 A 切换到任务 B 时,它必须保存任务 A 当前的状态(比如程序计数器指到哪行代码了、各个寄存器里存的临时变量是什么),然后加载任务 B 之前保存的状态。这就叫上下文切换。
  • 为什么进程切换更慢
    • 对于线程切换,由于它们属于同一个进程,共享同一个虚拟内存空间。所以只需要切换极少数的寄存器和栈指针。
    • 对于进程切换,由于每个进程有自己独立的虚拟内存空间。切换进程时,不仅要切换寄存器,更要命的是必须切换页表 (Page Table),从而导致 TLB (Translation Lookaside Buffer,页表缓存) 彻底失效并被清空。TLB 的失效会导致接下来的一段时间内,所有的内存访问都变慢(因为要去查慢速的主存),这是进程切换开销巨大的根本原因。