一、并发调度基础回顾:进程与线程的本质区别、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 调度示意图 线程 1 线程 2 线程 3 GIL(全局解释器锁) 同一时刻,只有一个线程能持有 GIL,其他线程等待 持有 GIL 的线程执行 100 条字节码后自动释放

看到没?同一时刻,只有一个线程能拿到 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 的三种并发模型都捋了一遍。这些东西看着简单,但真正用对地方,需要大量实践。我建议大家回去后,把上面的代码跑一遍,感受一下不同模型的行为差异。

想要更多资料,联系可可