最近学习Python的FastAPI框架的时候使用了很多Python相关的语法,虽然很多内容都和Java Spring那一套非常的类似,但是Python的并发模型和Java的区别还是非常大的
Python 不是不能多线程,真正的问题是 CPython解释器 有 GIL,所以它的并发策略和 Java 很不一样
总结:
- Python 可以多线程
- 但 CPython 有 GIL
- 所以 I/O 密集型 场景里,多线程和
async都很好用- 但是在CPU 密集型 场景里,多线程通常效果不好,更适合多进程
**Global Interpreter Lock(**全局解释器锁)
GIL 是 Global Interpreter Lock,全局解释器锁。
它的意思不是:Python 进程里只能有一个线程
它真正的意思是:同一时刻,只有一个线程能执行 CPython 解释器中的 Python 字节码
所以更准确的理解是:
- 线程可以开很多个
- 但是如果多个线程都在跑纯 Python 计算代码,它们会轮流抢 GIL
GIL 限制的不是"有没有线程",而是"多个线程能不能同时执行 Python 字节码"。但是因为这个是对解释器的限制但是没有办法解开的话其实是对并发是有很大的影响的。但是我听说好像有版本已经在尝试或者完成了GIL的移除,这个我没有了解过大家可以去搜搜看
为什么多进程可以解决GIL问题了?
很简单因为每一个进程都是有一个解释器的GIL的,各自去抢各自的锁,因此就每一个并发都不会影响彼此的。但是这样也有问题,最关键的一点就是进程通信的成本和线程通信的成本不是一个级别的。所以这里就是一个trade-off啦
Python的并发调用的两种写法
multiprocessing:多进程(绕开 GIL 的正统方法)
每个子进程有独立解释器 + 独立 GIL → CPU 密集型真正并行;代价是进程创建/IPC 序列化开销。
python
from multiprocessing import Process, Queue
def worker(i, q):
q.put((i, cpu_task()))
if __name__ == "__main__":
q = Queue()
procs = [Process(target=worker, args=(i, q)) for i in range(4)]
t0 = time.perf_counter()
for p in procs: p.start()
for p in procs: p.join()
results = [q.get() for _ in procs]
print(f"multiprocessing: {time.perf_counter() - t0:.2f}s")
threading:多线程(普通多线程)
关键点:CPython 中同一时刻只有一个线程能执行 Python 字节码 → CPU 密集型几乎不加速;但 IO 等待时 GIL 会被释放 → IO 密集型有效。
python
import threading
def run_threads(task, n=4):
results = [None] * n
def worker(i):
results[i] = task()
threads = [threading.Thread(target=worker, args=(i,)) for i in range(n)]
t0 = time.perf_counter()
for t in threads: t.start()
for t in threads: t.join()
print(f"threading: {time.perf_counter() - t0:.2f}s")
return results
asyc关键字(和Java进行对比)
Python的并发模型不是Java的1:1的而是更类似于Go的M:N模型
- async def:声明这个函数是个协程函数(coroutine function),调用它不会立即执行,只会返回一个"协程对象"(类似 Java 里的 Supplier<CompletableFuture<T>> 还没 .get() 的状态)。
- await:在协程里让出控制权,等另一个可等待对象(awaitable)完成,期间事件循环可以去跑别的协程。语义上最接近 Java 的 CompletableFuture.thenCompose / future.get(),但不阻塞线程。
python
async def hello(): # 协程函数
return 42
coro = hello() # ← 这里什么都没跑,只拿到协程对象
print(coro) # <coroutine object hello at 0x...>
import asyncio
asyncio.run(hello()) # 启动事件循环并跑到完成
async def f()≈ 返回CompletableFuture<T>的方法,但 Python 的协程是惰性的------不await也不run就根本不会执行。Java 的CompletableFuture.supplyAsync(...) 一调用就已经提交到线程池了,这是关键区别
python
# 方式1:同步函数 → FastAPI 会放到线程池里执行
@app.get("/sync")
def sync_route():
time.sleep(1) # 阻塞,但在线程池里,不影响其他请求
return {"msg": "sync"}
# 方式2:异步函数 → 直接在事件循环里执行
@app.get("/async")
async def async_route():
await asyncio.sleep(1) # 挂起,不阻塞事件循环
return {"msg": "async"}
在FastAPI里面两种方式的声明的区别是很大很大的
async带来的函数染色问题
在有async/await的语言里,函数被分成两种"颜色":
- 红色函数:async 函数,只能被另一个 async 函数调用(用 await)
- 蓝色函数:普通同步函数,谁都能调
规则:
- 红色函数不能被蓝色函数直接调用(同步代码里没法 await)
- 调用红色函数的成本远大于蓝色函数
- 一旦某个函数变红,调用链上所有函数都得跟着变红(传染性)
这就是为什么 Python / JavaScript / Rust 的 async 经常被吐槽------async 会向上传染整条调用栈,你写的同步库一旦想接入异步生态,就得整个改写或者包装。
为什么 Java 没有 GIL,而 Python 会有?
最主要的原因就是Java的GC Root的可达性分析的方法去进行GC操作的,但是Python是使用传统的引用计数的方式
CPython:引用计数让线程安全压力非常大
这意味着:
- 一个对象多一个引用,要
refcount + 1 - 一个对象少一个引用,要
refcount - 1 - 而这些操作会非常频繁地发生
如果多个线程同时修改这些对象状态,解释器内部就必须想办法保证线程安全。
GIL 就是一种非常粗但非常有效的方案:
- 不去给解释器内部无数细节都加锁
- 而是直接规定:同一时刻只有一个线程执行 Python 字节码
Java:不是引用计数,而是 tracing GC
Java 主要不是靠引用计数回收对象,而是靠 可达性分析,也就是从 GC Roots 出发做 tracing GC。
Java 平时运行时:
- 不需要每次对象引用变化都去维护一个全局共享
refcount - 因此不会像 CPython 一样,因为对象管理机制而天然倾向于引入一把 GIL 这种全局大锁
本质上就是因为这个GC机制的历史问题导致Python的GIL的移除非常的困难