并发基础概念:进程与线程、并发与并行、GIL详解
各位同学,咱们今天聊聊并发编程的根基。说实话,很多人在并发上栽跟头,不是因为代码写不对,而是基础概念没吃透。我见过太多面试者能把八股文背得滚瓜烂熟,一问到「你项目里到底怎么用的」,立马卡壳。
所以这一章,咱们不玩虚的。把进程、线程、并发、并行、GIL 这几个核心概念,掰开了揉碎了讲清楚。嗯,开始吧。
进程与线程:到底差在哪?
先问个问题:你打开一个浏览器,又打开一个,这两个东西能同时跑,靠的是什么?
答案是进程。每个应用程序启动时,操作系统会为它分配一块独立的内存空间、文件句柄、寄存器状态等资源。这就是一个进程。进程之间是隔离的,一个进程挂了,一般不会影响另一个。我在项目中遇到过,某个第三方库的内存泄漏导致整个服务 OOM,但其他进程毫发无损——这就是进程隔离的好处。
那线程呢?线程是进程内部的执行单元。一个进程可以包含多个线程,它们共享进程的内存空间。说白了,线程就是「轻量级的执行流」。创建线程的开销比进程小得多,切换也快。
我习惯用一个比喻来理解:进程就像一家公司,有自己的办公楼(内存)、财务(资源)、安保(权限)。线程就是公司里的员工,大家在同一栋楼里办公,共享会议室、茶水间。但问题也来了——员工之间抢资源怎么办?这就是并发编程要解决的。
核心区别总结:
- 资源隔离:进程独立,线程共享
- 创建开销:进程大,线程小
- 通信方式:进程间通信(IPC)复杂,线程间通信简单(共享内存)
- 稳定性:进程间互不影响,线程间一个崩溃可能拖垮整个进程
我的经验:如果你需要高隔离性(比如运行不可信代码),用多进程。如果你追求高并发和低延迟,多线程更合适。但记住,Python 的多线程受 GIL 限制,后面会细说。
并发与并行:别再傻傻分不清
这两个词经常被混用,但本质完全不同。我刚开始学的时候也搞混过,直到有一次在代码 review 中被前辈点醒。
并发(Concurrency):是逻辑上的「同时」。一个 CPU 核心在多个任务之间快速切换,看起来像同时执行。你想想看,你一边写代码一边听音乐,其实你的大脑在快速切换注意力——这就是并发。
并行(Parallelism):是物理上的「同时」。多个 CPU 核心真正同时执行多个任务。就像你有两个大脑,一个写代码,一个听音乐,互不干扰。
为什么会这样?因为并发解决的是「如何让程序看起来在同时做多件事」,而并行解决的是「如何让程序真正同时做多件事」。并发是结构问题,并行是执行问题。
避坑指南:我曾经在一个项目中,天真地以为开了 100 个线程就能让任务快 100 倍。结果发现 CPU 只有 4 个核心,大部分时间都花在线程切换上了。嗯,这就是把并发当成了并行。记住:并发不一定会加速,并行才会。
用一张图来理解:
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 密集型场景依然好用。