进程是餐厅,线程是厨师

开篇除恐

当人们把“进程”和“线程”两个概念混在一起谈论时,往往会让人觉得它们是某种高深的操作系统魔法,好像只有读了几百页的教材才能分清。其实,只要把它们想象成熟悉的餐厅场景,一切就迎刃而解:进程就是一家独立的餐厅,线程则是餐厅里为顾客服务的厨师。一旦有了这个具体画面,你就不必再被术语吓倒——你只需要记住,餐厅之间互不干扰,而厨师们可以共用同一间厨房。

白话化

  • 进程(Process):拥有独立的地址空间(自己的库房、冷冻柜、菜谱本),以及自己的一套系统资源(文件句柄、网络端口等)。不同进程之间默认不能直接访问对方的内存,除非通过特殊的进程间通信(IPC)机制。
  • 线程(Thread):属于某个进程,共有该进程的地址空间和资源。线程有自己的程序计数器、栈和寄存器状态,但可以读写进程里的全局变量、堆内存等——就像厨师们都能使用同一套案板、炉灶和调料罐。

换言之,进程是资源的容器,线程是容器内部实际执行任务的工人

直觉先行

想象你在写一个简单的聊天程序: - 程序启动时,操作系统为它创建一个进程,分配给它一块内存空间(用来存放代码、全局变量、堆等)。 - 在这个进程里,你又启动了两个线程: - 一条负责从网络 socket 读取消息并显示; - 另一条负责读取键盘输入并发送出去。

这两条线程都在同一个进程的地址空间里工作,因而可以直接共享比如“聊天记录缓冲区”、“用户名”这样的变量——无需额外的复制或消息传递。如果换成两个独立的进程(比如分别运行两个可执行文件),则它们各自拥有自己的一套内存,想要共享数据就得通过管道、共享内存、消息队列等较为笨重的 IPC 手段。

因此,线程的首要好处就是:在同一份资源池里实现轻量级的并发,而进程则提供了强隔离,防止一个任务的崩溃牵连另一个。

例子贴身

例子 1(☼ 热身):查看进程与线程号(跨平台)

任务:写一个小程序,打印出自身的进程 ID(PID)和线程 ID(TID),帮助直观感受“进程是容器,线程是其中的执行单元”。

怎么想到的:我们可以使用 os.getpid() 获得进程号,threading.get_ident() 获得线程号。打印出来后,我们就能看到:同一进程里的不同线程有相同的 PID 但不同的 TID。

示例代码(Python)

import os
import threading

def thread_func():
    print(f"In thread: PID = {os.getpid()}, TID = {threading.get_ident()}")

def main():
    print(f"In main:  PID = {os.getpid()}, TID = {threading.get_ident()}")
    t = threading.Thread(target=thread_func)
    t.start()
    t.join()

if __name__ == "__main__":
    main()

运行结果(示例)

In main:  PID = 12345, TID = 140123456789120
In thread: PID = 12345, TID = 140123456780480

要点:PID 完全相同,说明两者属于同一个进程;TID 不同,说明是两个独立的执行流。

例子 2(☼☼ 正经):资源隔离演示(进程 vs 线程)

任务:展示进程之间默认不能直接修改对方的内存,而线程之间可以。

怎么想到的:我们先创建一个共享的整型变量 shared = 0。然后: - 线程版:主线程和子线程都对 shared 进行 += 1 操作,最后读取可以看到累加效果; - 进程版:使用 multiprocessing.Process 创建子进程,子进程对自己的 shared 副本进行修改,父进程读取时发现自己的值未变(因为子进程得到了一份拷贝)。

示例代码(Python)

import threading
import multiprocessing
import time

# 线程版演示
def thread_worker(shared_counter):
    for _ in range(1000):
        shared_counter[0] += 1

def thread_demo():
    shared = [0]  # 用列表使其可在线程间修改
    t = threading.Thread(target=thread_worker, args=(shared,))
    t.start()
    # 主线程也加
    for _ in range(1000):
        shared[0] += 1
    t.join()
    print(f"[Thread] final shared = {shared[0]}")  # 期望 2000

# 进程版演示
def process_worker(shared_counter):
    for _ in range(1000):
        shared_counter[0] += 1

def process_demo():
    shared = multiprocessing.Array('i', [0])  # 共享内存数组,这里为了演示隔离我们不使用它
    # 为了展示默认隔离,我们使用普通变量在子进程中是独立的副本
    def worker():
        local_shared = 0
        for _ in range(1000):
            local_shared += 1
        print(f"[Child]  shared = {local_shared}")
    p = multiprocessing.Process(target=worker)
    p.start()
    p.join()
    # 父进程的共享变量仍为 0
    print("[Parent] shared = 0")  # 应该仍为 0(未继承子的修改)

def main():
    print("=== 线程版 ===")
    thread_demo()
    print("\n=== 进程版 ===")
    process_demo()

if __name__ == "__main__":
    main()

运行结果(示例)

=== 线程版 ===
[Thread] final shared = 2000

=== 进程版 ===
[Child]  shared = 1000
[Parent] shared = 0

要点
- 线程之间对 shared 的修改是可见的,因为它们共享同一份内存(这里通过列表引用实现)。
- 进程之间(这里通过 multiprocessing.Process 得到的子进程)在默认情况下拥有独立的内存副本;子进程对变量的改动不会反映到父进程。若真正想在进程间共享数据,需要使用 multiprocessing.Array, multiprocessing.Value, 或其他显式的共享内存机制。

收尾

这一章要带走的东西
- 进程是拥有独立资源(内存、文件句柄等)的容器,类比一家独立的餐厅。
- 线程是进程内部轻量级的执行单元,共享该进程的地址空间和资源,类比餐厅里的厨师。
- 同一进程的多个线程可以通过全局变量、堆对象等直接通信;不同进程之间默认隔离,需要额外的 IPC 机制才能交换数据。
- 因此,当你需要轻量级并发且数据需要频繁共享时,优先考虑线程;当你需要强隔离、防止崩溃蔓延时,考虑使用多进程。

就这样。 接下来,我们将探讨即便是单核 CPU,也为何能让多个线程“同时”运行——即时间片轮转和调度的基本原理。