并发基础概念:进程与线程、并发与并行、GIL详解

各位同学,咱们今天聊聊并发编程的根基。说实话,很多人在并发上栽跟头,不是因为代码写不对,而是基础概念没吃透。我见过太多面试者能把八股文背得滚瓜烂熟,一问到「你项目里到底怎么用的」,立马卡壳。

所以这一章,咱们不玩虚的。把进程、线程、并发、并行、GIL 这几个核心概念,掰开了揉碎了讲清楚。嗯,开始吧。

进程与线程:到底差在哪?

先问个问题:你打开一个浏览器,又打开一个,这两个东西能同时跑,靠的是什么?

答案是进程。每个应用程序启动时,操作系统会为它分配一块独立的内存空间、文件句柄、寄存器状态等资源。这就是一个进程。进程之间是隔离的,一个进程挂了,一般不会影响另一个。我在项目中遇到过,某个第三方库的内存泄漏导致整个服务 OOM,但其他进程毫发无损——这就是进程隔离的好处。

那线程呢?线程是进程内部的执行单元。一个进程可以包含多个线程,它们共享进程的内存空间。说白了,线程就是「轻量级的执行流」。创建线程的开销比进程小得多,切换也快。

我习惯用一个比喻来理解:进程就像一家公司,有自己的办公楼(内存)、财务(资源)、安保(权限)。线程就是公司里的员工,大家在同一栋楼里办公,共享会议室、茶水间。但问题也来了——员工之间抢资源怎么办?这就是并发编程要解决的。

核心区别总结:

  • 资源隔离:进程独立,线程共享
  • 创建开销:进程大,线程小
  • 通信方式:进程间通信(IPC)复杂,线程间通信简单(共享内存)
  • 稳定性:进程间互不影响,线程间一个崩溃可能拖垮整个进程

我的经验:如果你需要高隔离性(比如运行不可信代码),用多进程。如果你追求高并发和低延迟,多线程更合适。但记住,Python 的多线程受 GIL 限制,后面会细说。

并发与并行:别再傻傻分不清

这两个词经常被混用,但本质完全不同。我刚开始学的时候也搞混过,直到有一次在代码 review 中被前辈点醒。

并发(Concurrency):是逻辑上的「同时」。一个 CPU 核心在多个任务之间快速切换,看起来像同时执行。你想想看,你一边写代码一边听音乐,其实你的大脑在快速切换注意力——这就是并发。

并行(Parallelism):是物理上的「同时」。多个 CPU 核心真正同时执行多个任务。就像你有两个大脑,一个写代码,一个听音乐,互不干扰。

为什么会这样?因为并发解决的是「如何让程序看起来在同时做多件事」,而并行解决的是「如何让程序真正同时做多件事」。并发是结构问题,并行是执行问题。

避坑指南:我曾经在一个项目中,天真地以为开了 100 个线程就能让任务快 100 倍。结果发现 CPU 只有 4 个核心,大部分时间都花在线程切换上了。嗯,这就是把并发当成了并行。记住:并发不一定会加速,并行才会。

用一张图来理解:

并发(单核) 任务A 任务B 任务C 任务D 时间轴 → 快速切换 并行(多核) 核心1:任务A 核心2:任务B 核心3:任务C 时间轴 → 真正同时执行

GIL 全局解释器锁:Python 并发的心头痛

说到 Python 并发,GIL 是绕不开的坎。GIL 全称 Global Interpreter Lock,全局解释器锁。它的存在让 Python 的多线程在 CPU 密集型任务上几乎毫无优势。

GIL 是什么?说白了,就是 Python 解释器里的一把大锁。它保证同一时刻只有一个线程在执行 Python 字节码。你想想看,你开了 8 个线程,但 CPU 一次只能跑一个线程的代码——这不就退化成了并发吗?

为什么会这样?历史原因。Python 的内存管理不是线程安全的。早期设计者为了简单,直接加了一把全局锁。这样一来,多线程编程就简单了,但性能也牺牲了。

GIL 的影响:

  • CPU 密集型任务:多线程几乎没用,甚至更慢(因为锁竞争和切换开销)
  • I/O 密集型任务:多线程仍然有效,因为 I/O 操作会释放 GIL
  • 解决方案:使用多进程(multiprocessing)或异步编程(asyncio)

来看个代码示例,直观感受一下:

import threading
import time

def cpu_bound_task(n):
    """CPU 密集型任务:计算斐波那契数列"""
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

# 单线程版本
start = time.time()
for _ in range(4):
    cpu_bound_task(100000)
print(f"单线程耗时: {time.time() - start:.2f}s")

# 多线程版本
threads = []
start = time.time()
for _ in range(4):
    t = threading.Thread(target=cpu_bound_task, args=(100000,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(f"多线程耗时: {time.time() - start:.2f}s")

运行结果会让你大跌眼镜:多线程版本不仅没快,反而可能更慢。这就是 GIL 的「功劳」。

我的建议:在 Python 中做 CPU 密集型并发,直接用 multiprocessing 模块。它创建的是进程,每个进程有自己的 GIL,可以真正并行。但要注意进程间通信的开销。

那 I/O 密集型任务呢?比如网络请求、文件读写。这些操作在等待 I/O 时会释放 GIL,其他线程就能趁机执行。所以多线程在 I/O 密集型场景下依然有效。我曾经用多线程写过一个爬虫,开了 20 个线程,抓取速度提升了近 10 倍——因为大部分时间都在等网络响应。

避坑指南:我曾经在项目中用多线程处理大量计算任务,结果发现 CPU 占用率只有 100%(单核满载),其他核心都在摸鱼。排查了半天才意识到是 GIL 的问题。后来改用多进程,CPU 占用率直接飙到 400%,任务完成时间缩短了 3 倍。嗯,这个教训很深刻。

最后总结一下 GIL 的应对策略:

任务类型 推荐方案 原因
CPU 密集型 多进程(multiprocessing) 每个进程独立 GIL,可并行
I/O 密集型 多线程(threading)或异步(asyncio) I/O 等待时释放 GIL,线程可切换
混合型 多进程 + 异步 进程处理计算,异步处理 I/O

好了,这一章的内容就到这里。进程与线程的区别、并发与并行的本质、GIL 的来龙去脉,都讲清楚了。记住:基础不牢,地动山摇。把这些概念吃透了,后面的并发编程之路才能走得稳。

一句话总结:进程是资源隔离的单位,线程是执行调度的单位;并发是逻辑上的同时,并行是物理上的同时;GIL 让 Python 多线程在 CPU 密集型任务上「名存实亡」,但 I/O 密集型场景依然好用。

可可精品资料库