异步 IO
进程与线程
- 在我们在终端用
python命令启动一个程序的时候,就是创建了一个python进程 - 进程包含一个或者多个线程,以及其他各种资源。
- 真正执行代码的是线程
- 可以把进程想象成一个公司,线程就是这个公司里面的员工。公司除了员工,当然还有其他很多东西。
- 一个进程至少有一个线程,这个线程叫做主线程
- 如果程序不包含
threading库等,那么这个进程一般只有一个线程,就是主线程
- 如果程序不包含
- 一个CPU核心在同一时间只能执行一个线程
- 超线程也是这样的,超线程只是用了更高级的切换线程的方式
- 一个进程的线程可以在不同的CPU核心中被执行,哪个核心有空就在哪个核心执行
- 所以其实进程数目不是根本因素,线程数目才是根本因素。看CPU是否有空闲/利用率是否高,就看其是否一直在执行线程
同步与异步,阻塞与非阻塞
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。 1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。 3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
这个例子中,老张相当于线程,水壶相当于IO请求,所以就是说同步异步就是指请求是否通知线程(关注的是消息通知机制),阻塞非阻塞就是指线程是否继续执行(关注的是调用者(线程)在等待结果时的状态)
asyncio库
- 在函数前加上
async可以把函数变成协程- 调用协程不会执行函数,只会得到一个协程对象
- 协程对象的执行需要通过
asyncio.create_task()和asyncio.gather()等
asyncio.run()的作用是:启动异步程序,运行一个顶级协程,并在结束后自动关闭事件循环asyncio.create_task()的作用是把协程包装成一个 Task 并立即调度到事件循环中并发执行,从而让它“后台”运行,而不必等待它完成才能继续往下走asyncio.gather()的作用是并发启动多个协程,等它们全部完成,然后把所有返回值按顺序收集成一个列表返回await后面可以跟任何语句,语法上是允许的,但是一般只跟三种语句- 真正的IO请求
- 显式交出控制权的语句
- 内部包含
await的语句(也就是嵌套await)
asyncio库不会创建其他进程或者线程- 创建进程是
multiprocessing库 - 创建线程是
threading库 asyncio库仍然是单线程执行,只不过不同的Task轮流执行
- 创建进程是
下面是一些具体的例子:
-
import asyncio from openai import AsyncOpenAI client = AsyncOpenAI( api_key="sk-1234", base_url="http://0.0.0.0:4000" ) async def chat(message: str) -> str: """单个异步请求""" response = await client.chat.completions.create( model="my-model", messages=[{"role": "user", "content": message}] ) return response.choices[0].message.content async def main(): # 定义多个独立的请求 messages = [ "What LLM are you?", "Could you please tell 1+1 =?", "I want to make friends with you.", "Tell me a joke.", "What is Python?", ] # 使用 asyncio.gather 并发执行所有请求 tasks = [chat(msg) for msg in messages] results = await asyncio.gather(*tasks) for msg, result in zip(messages, results): print(f"Q: {msg}\nA: {result}\n") # 运行 asyncio.run(main())- 线程一直执行到
tasks = [chat(msg) for msg in messages],然后这个语句创建了若干协程对象 results = await asyncio.gather(*tasks)await将main函数挂起,意思是等到asyncio.gather(*tasks)执行结束之后再继续执行下面的代码asyncio.gather(*tasks)会把这些协程对象封装成 Task(如果还没封装的话),并注册到事件循环中- 同一时刻线程只能够执行一个Task
- 当线程执行到某个Task的
await client.chat.completions.create的时候,await会将当前Task挂起,控制权交还给事件循环,事件循环会查看“还有谁准备好了?”,然后让线程去执行下一个 Task - 最后所有协程对象执行都完毕了之后,将所有协程对象的执行结果按序放到
results中
- 线程一直执行到
-
import asyncio async def chat(msg: str) -> str: print(f"开始处理: {msg}") await asyncio.sleep(1) # 模拟网络请求 print(f"完成处理: {msg}") return f"回复: {msg}" async def main(): messages = ["A", "B", "C"] print("=== 串行执行 ===") for msg in messages: result = await chat(msg) print(result) asyncio.run(main())- 线程执行到
result = await chat(msg)的时候,await让main挂起,线程去继续执行chat - 线程执行到
await asyncio.sleep(1)的时候,会将chat挂起 - 这是一个嵌套
await,嵌套链上的代码全部会挂起,所以相当于串行执行(也就是将这个代码中的所有异步全部取消都没有啥变化)
- 线程执行到
可以使用multiprocessing库开多个进程,每个进程里面使用asyncio库进行异步IO以最大化利用CPU资源

浙公网安备 33010602011771号