一、什么是并行与并发(进程、线程和协程)
1.1、定义:
并行(Parallelism): 多个任务在同一时刻真正同时执行
并发(Concurrency): 多个任务在同一时间段内交错推进,系统在它们之间快速切换,使得宏观上看起来“同时进行”
1.2、举例说明:
想象你开餐厅,需要同时做3道菜:红烧肉、蒸鱼和炖汤
模式1:进程——开3家餐厅(并行)
- 在3个地点开了3家餐厅(3个进程)
- 每家餐厅都有自己的厨房、厨师和食材(独立内存空间)
- 三家餐厅可以同时开工,互不影响
- 优点:一家店出问题(比如进程崩溃),不会影响其他餐厅运营(隔离性好)
- 缺点:成本高,资源浪费
模式2:线程——一个厨房,多个厨师(并发,受限并行)
-
只有一家餐厅,但是有3个厨师(3个线程)
-
但是一个厨房,3个厨师共用(共享内存空间)
-
关键问题:
- 线程在CPU 上理论上可以实现并行(就像多个厨师同时在厨房干活),但在 CPython 解释器中,由于存在一个叫 GIL(全局解释器锁)的“总管”,它规定:同一时刻,只能有一个线程执行 Python 代码(即只有一个厨师能拿着菜谱在厨房操作)。
- 因此,对于 CPU 密集型任务(比如切菜、炒菜等需要持续计算的操作),多个线程无法真正并行,只能轮流使用厨房,实际仍是串行执行。
- 但是,当某个线程执行 I/O 操作(比如把肉拿出来化冻、等水烧开、等待网络响应)时,它会主动释放 GIL 并进入等待状态。这时,“经理”(操作系统或 Python 解释器)就会让另一个线程进入厨房继续工作。
- 通过这种方式,多个线程可以在 I/O 等待期间交替执行,从而实现并发——虽然不是真正的并行,但在 I/O 密集型场景下能显著提升效率。
-
什么情况下,换厨师进入厨房进行工作(切换任务):
1)时间片用完
- 比如经理分给每个厨师0.5个小时用厨房,到时间就换厨师
2)线程主动阻塞
- 比如菜化冻,等水烧开等需要等待的任务时,换厨师进行工作
- 缺点: 一个线程崩溃,通常会导致整个进程崩溃
模式3:协程——一个厨师,合理规划任务(并发)
-
只有一个餐厅(进程),一个厨师(线程)
-
他会合理规划任务,能在任务间快速切换
-
示例:
- 从冰箱中取出菜化冻(I/O等待),此时不等化冻完毕,先去切葱
- 当化冻完成的通知到达(I/O完成),自动切回该任务,洗菜
- 烧油
- 将葱和菜放入锅中进行炒菜
-
关键点:
- 厨师(线程)在同一时间只做一件事
- 但通过主动让出(await)和事件通知,实现并发
- 因为整个厨房只有他一个人干活,不需要和其他厨师协调资源,所以不存在 GIL 争用问题(GIL 依然存在,但不会成为瓶颈)
- 任务切换由程序自己控制,无需操作系统介入,因此上下文切换开销极小,非常适合高并发的 I/O 密集型场景
1.3、线程和协程实现并发的区别:
根据上述内容,线程和协程都可以实现并发,那么使用线程和协程实现并发具体有什么区别呢?
二、异步函数与事件循环
2.1、异步函数
(1)定义:
异步函数(async function) 是一种特殊的函数,它使用 "async def" 定义,调用时不会立即执行函数体,而是返回一个协程对象(coroutine)。
它允许程序在等待耗时操作(如网络请求、文件读写)时,主动让出控制权,去执行其他任务,从而实现高并发
(2)基本语法与注意事项
async def my_async_function():
# 异步操作,例如 await 网络请求
result = await some_async_operation()
return result
注意:
- "await"后必须是一个可等待对象,"await"告诉python,需要等待"await"后的代码完成并返回结果,在此期间,会让出控制权,python可以先执行其它代码,当“await”后的代码执行完毕,返回结果后,再切换回来,执行后续代码
- 要使"await"工作,它必须支持这种异步机制的函数内,及"await"必须位于使用"async def"声明的函数(异步函数)中,而执行异步函数,又必须在前添加"await",否则只会返回一个协程对象,而不是函数执行的结果。
- 这就带来了一个问题,"await"必须位于异步函数中,而执行异步函数又需要添加"await",变成了先有鸡还是先有蛋的问题,带来疑问,该如何执行最顶层的异步函数? 具体参见下一节"事件循环"。
2.2、事件循环
(1)定义:
一个持续运行的循环机制,是 asyncio 的核心调度器,负责:
- 接收并管理待执行的协程(coroutines);
- 当协程遇到"await"时,将其挂起,并调度其他就绪的协程;
- 当被等待的异步操作(如 I/O)完成后,恢复对应协程的执行
(2)事件循环启动异步函数:
在包含异步函数的脚步中,对于最顶层的异步函数/主函数,需要启动一个事件循环,通过事件循环运行函数,例如:
"asyncio.run(main())"实现了以下效果:
- 创建一个事件循环。
- 把"main()" 这个协程(coroutine)放入循环中运行。
- 当"main()" 结束时,关闭事件循环。
常见错误: 直接调用异步函数
如果你这样写:
main()
你会得到警告或什么也不发生,因为"main()"返回的是一个 协程对象(coroutine object),而不是执行它。
(3)不同场景启动最顶层异步函数的区别
重点:
1)Jupyter 的单元格执行器内置了事件循环支持,可以自动处理顶层的"await",不需要手动启动事件循环
当你在jupyter中执行以下代码:
result = await some_async_function()
print(result)
jupyter的内核(Kernel)实际上在执行时做了类似这样的事:
# 伪代码:Jupyter 内部的执行逻辑
async def hidden_main():
result = await some_async_function()
print(result)
# Jupyter 自动为你运行
asyncio.run(hidden_main())
2)在FastAPI等Web框架中,服务器启动事件循环
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/slow")
async def slow_endpoint():
await asyncio.sleep(5)
return {"message": "Done!"}
- 你不需要写"asyncio.run()"
- 当你用"uvicorn main:app --reload"启动时:
- Uvicorn 会创建一个或多个工作进程。
- 在每个工作进程中,它会启动一个事件循环