一、并发调度基础回顾:进程与线程的本质区别、GIL锁的前世今生、Python并发模型概览
各位同学,咱们今天正式开课。说实话,每次讲并发调度,我都要先问自己一个问题:你真的理解进程和线程吗? 很多同学写了好几年 Python,一问到 GIL 就支支吾吾。嗯,这很正常。我当年刚入行时,也被这玩意儿坑过不少次。
今天这一章,咱们就把地基打牢。说白了,后面所有的高并发技巧,都建立在这些基础概念之上。你想想看,如果连进程和线程的本质区别都说不清楚,那调度策略、锁优化什么的,就更别谈了。
1.1 进程与线程:本质区别到底在哪?
先问个问题:进程和线程,哪个更“重”? 答案当然是进程。但“重”在哪里?我给大家拆开讲。
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立的内存空间、文件句柄、信号量等 | 共享所属进程的资源 |
| 切换开销 | 需要切换页表、刷新 TLB,开销大 | 只需保存/恢复寄存器,开销小 |
| 通信方式 | IPC(管道、消息队列、共享内存等) | 直接读写共享变量(需加锁) |
| 创建速度 | 慢(fork 或 CreateProcess) | 快(通常几微秒) |
| 隔离性 | 强,一个进程崩溃不影响其他 | 弱,一个线程崩溃可能导致整个进程挂掉 |
我个人习惯用一句话总结:进程是资源分配的最小单位,线程是 CPU 调度的最小单位。 你想想看,操作系统调度时,它关心的是线程,而不是进程。进程只是给线程提供了一个“工作环境”。
核心要点: 线程之间共享内存,所以通信快,但需要同步机制(锁)。进程之间隔离,所以安全,但通信慢。选哪个,取决于你的场景。
我在项目中遇到过这样一个坑:一个数据处理服务,用了多线程,结果因为共享变量没加锁,数据全乱了。后来改成多进程 + 消息队列,虽然慢了点,但至少正确了。嗯,这就是典型的“用错模型”的教训。
1.2 GIL 锁的前世今生
说到 Python 并发,GIL 是绕不开的话题。很多新手一听到 GIL 就头疼,觉得它是 Python 的“原罪”。其实不然。咱们先看看 GIL 是怎么来的。
GIL 的全称是 Global Interpreter Lock,全局解释器锁。 它的设计初衷很简单:保证 CPython 解释器的内存安全。 因为 CPython 的内存管理不是线程安全的,如果没有 GIL,多个线程同时操作 Python 对象,内存分分钟就崩了。
我给大家画个图,看看 GIL 是怎么工作的:
看到没?同一时刻,只有一个线程能拿到 GIL。 其他线程只能干等着。这就是为什么 Python 多线程在 CPU 密集型任务上表现不佳——说白了,它根本没法真正并行。
注意: GIL 只影响 CPython 实现。Jython、IronPython 没有 GIL。另外,I/O 密集型任务受 GIL 影响较小,因为线程在等待 I/O 时会主动释放 GIL。
我曾经在一个爬虫项目里,用多线程抓取 1000 个网页。一开始以为线程越多越快,结果开了 50 个线程,CPU 占用率才 30%。后来换成多进程 + asyncio,速度直接翻了 4 倍。嗯,这就是不懂 GIL 的代价。
1.3 Python 并发模型概览
Python 的并发模型,说白了就三种:多线程、多进程、异步 I/O。 咱们一个一个看。
1.3.1 多线程(threading)
适合 I/O 密集型任务。比如网络请求、文件读写。因为线程在等待 I/O 时会释放 GIL,其他线程可以趁机执行。但要注意:多线程不能利用多核 CPU 做并行计算。
import threading
import time
def worker(name):
print(f"线程 {name} 开始工作")
time.sleep(2) # 模拟 I/O 操作
print(f"线程 {name} 结束工作")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
1.3.2 多进程(multiprocessing)
适合 CPU 密集型任务。每个进程有独立的 GIL,可以真正并行。但进程间通信开销大,创建成本也高。
from multiprocessing import Process
import os
def cpu_task(n):
print(f"进程 {os.getpid()} 正在计算 {n} 的平方")
return n * n
if __name__ == '__main__':
processes = []
for i in range(4):
p = Process(target=cpu_task, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
1.3.3 异步 I/O(asyncio)
适合高并发 I/O 场景。单线程内通过事件循环调度协程,没有线程切换开销。但代码写法比较特殊,需要 async/await 关键字。
import asyncio
async def fetch_data(url):
print(f"开始请求 {url}")
await asyncio.sleep(1) # 模拟网络请求
print(f"完成请求 {url}")
return f"数据来自 {url}"
async def main():
tasks = [fetch_data(f"http://example.com/{i}") for i in range(5)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
我的建议: 选模型之前,先问自己三个问题:1)任务是 CPU 密集还是 I/O 密集?2)需要共享大量数据吗?3)并发量有多大?答案不同,选型就不同。
1.4 三种模型对比
| 模型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 多线程 | I/O 密集型,中等并发 | 编程简单,共享数据方便 | GIL 限制,不适合 CPU 密集 |
| 多进程 | CPU 密集型,高隔离性 | 真正并行,进程隔离 | 创建成本高,IPC 复杂 |
| 异步 I/O | 高并发 I/O,大量连接 | 单线程无锁,极高并发 | 代码难写,调试困难 |
好了,这一章的内容就到这里。咱们把进程线程的本质、GIL 的来龙去脉、以及 Python 的三种并发模型都捋了一遍。这些东西看着简单,但真正用对地方,需要大量实践。我建议大家回去后,把上面的代码跑一遍,感受一下不同模型的行为差异。
想要更多资料,联系可可