Python的异步编程:asyncio
基础知识
并发和并行的核心区别主要在于 是否真正同时执行任务,一个 CPU 核心可以实现并发(交替执行),但不能实现并行。多个 CPU 核心才可以实现并行。
并发
并发是多个任务之间进行快速切换,使它们看起来像是同时进行的,用于单核 CPU。在一个时间段内,多个任务可以被交替执行。 这些任务并非真正同时运行,而是通过极快的时间片轮转或任务切换实现“同时进行”的效果。
适用于 I/O 密集型任务(如网络请求、数据库访问),在网络请求或者数据库访问的信息没回来之前,切换执行其他任务,而不是干等着某一个任务完成。主要由 多线程 或 协程(Coroutine) 实现。
假设正在做饭,同时在等待烧水和炒菜,打开水壶烧水,等待烧开(但不会一直盯着),在等水开的时间里,开始炒菜。水开了,回来泡茶,然后继续炒菜。虽然在“同时”完成烧水和炒菜,但实际上 CPU 只是在任务之间快速切换。
并行
并行是指多个任务在同一时刻真正地同时执行。 需要多个 CPU 核心(多核处理器)或多台计算机协同工作。主要由 多进程(Multiprocessing) 实现。假设和朋友一起做饭,我烧水,他炒菜,两个人真正同时工作,这样可以比一个人轮流完成更快。
同步
同步是指任务按照顺序执行,下一个任务必须等待上一个任务完成后才能开始。每个任务必须等前一个任务完成后才能执行,导致总时间较长。单线程。
假设做饭:先烧水,等水开。水开后,再炒菜。炒完菜,再泡茶。所有任务严格按顺序执行,不能交叉。
异步
异步(Asynchronous) 是一种编程方式,它允许任务在等待的时候去执行其他任务,而不会阻塞整个程序的运行。不必等待前一个任务完成后再执行。主要用于 I/O 密集型任务,如网络请求、数据库查询等,和并发的概念有一点像!
核心特点:当某个任务在等待响应时(如 I/O 操作),程序不会停下来,而是转去执行其他任务,直到要等待响应的任务完成并取得结果之后,再继续处理。
任务可以在等待时交替执行,提高效率。适用于 I/O 密集型任务(爬虫、API 调用)。主要通过 async/await + 协程 实现。
做饭时,打开水壶烧水,不用等它开,就去切菜。切完菜,水也快开了,可以直接泡茶,然后炒菜。
进程
进程是独立运行的程序,每个进程都有自己的内存空间,不能直接共享数据。进程间相互独立,一个崩溃不会影响其他进程。适合 CPU 密集型任务,如数据分析、机器学习等。
一个应用程序可能有 一个进程,也可能包含多个进程,具体取决于应用程序的设计。
- 单进程应用:一个应用程序只有一个进程,所有任务都在这个进程里运行。如命令行终端(CMD、终端),运行时只有一个进程,执行命令时也是在同一个进程内完成。如果进程崩溃,整个程序都会崩溃。
- 多进程应用: 一个应用程序可以创建多个进程,每个进程执行不同的任务。Google Chrome(多进程浏览器),每个标签页(Tab)是一个独立进程,如果一个标签页崩溃,不会影响其他标签页,还有渲染进程、GPU 进程、网络进程等多个进程。微信、QQ有主进程:管理界面、窗口。有网络进程:负责消息发送和接收。有音视频进程:负责语音、视频通话。进程间隔离,如果某个进程崩溃,不会影响整个程序。利用多核 CPU,多个进程可以并行运行,提高性能。
线程
线程是进程内部的执行单元,多个线程共享进程的资源(内存、文件等)。线程是轻量级的,崩溃可能影响整个进程,切换快,开销小,同一进程的线程共享内存、变量、文件句柄等数据信息。
线程间通信快,但需要同步机制(如锁 Lock)。适合 I/O 密集型任务,如网络爬虫、数据库查询等。
并发、并行、同步、异步 既可以基于 线程,也可以基于 进程,它们的实现方式和适用场景有所不同。
示例
餐厅经营
假设我是一家餐厅的老板,我有 多个厨师(线程) 和 多个厨房(进程)。
🔹 进程 = 独立的厨房
- 开了一家餐厅 (进程),里面有自己的厨房,完全独立。
- 另一家餐厅(另一个进程)不能直接访问你的厨房,只能通过送外卖(进程间通信)来传递信息。
- 如果一个厨房(进程)着火了,不会影响其他餐厅(进程)。
特点:每个进程都有自己的资源,互不干扰,无法直接共享数据。
🔹 线程 = 一个厨房里的多个厨师
- 我雇了多个厨师(线程),他们共享厨房(共享内存),可以同时做菜。
- 但是如果多个厨师同时用同一个锅(变量),就可能发生冲突(数据竞争),需要有管理规则(锁)。
- 如果一个厨师(线程)出了问题,可能会影响整个厨房(进程)。
特点:同一进程内的线程共享资源,但容易相互影响,需要同步控制。
学校教学
🔹 进程:不同的学校
- 每所学校(进程)都是独立的,有自己的教学楼、教师和学生。
- 不同学校之间不能随便互相借课本(进程不能直接共享数据)。
🔹 线程:一个学校的多个老师
- 在同一个学校(进程)里,有多个老师(线程)。
- 老师们共享学校的资源(比如教室、课本)。
- 但如果多个老师同时要用一块黑板,就需要排队(同步机制)。
如果应用程序需要同时运行多个独立的任务(如 Chrome 多个标签页) 👉 多进程
如果任务需要共享数据,且是 I/O 密集型(如爬虫、网络请求) 👉 多线程
如果任务是 CPU 密集型(如深度学习) 👉 多进程
Python异步编程
异步编程的两大核心: 事件循环 + 协程函数
在 Python 里,所谓的异步函数 其实就是指 协程函数,既可以叫 协程函数,也可以叫 异步函数,其实是同一个概念,没什么区别。
所谓「异步 IO」,就是发起一个 IO 操作,但不用等它运行结束,在它运行期间可以继续做其他事情,当它结束时,会得到相应的事件通知。
Asyncio 采取的是并发(concurrency)方式。Asyncio 并不能带来真正的并行(需要多核CPU)。因为 GIL(全局解释器锁)的存在,Python 的多线程也不能带来真正的并行。
可以交给 asyncio 执行的任务称为协程(coroutine)。一个协程可以放弃执行(存在等待结果的过程时),把CPU的运行让给其它协程(即 await)
什么是asyncio?
使用
async def定义协程函数(异步函数),await用于等待异步操作完成。
asyncio 是 Python 内置的 异步编程库,用于 异步 I/O 操作。它可以让程序在等待 I/O(如网络请求、文件读写、数据库查询)时,不阻塞主线程,从而提高运行效率。
在普通的 同步 程序中,遇到 I/O 操作(比如等待网络请求返回)时,程序必须 等待,浪费了 CPU 资源。而 asyncio 可以让程序在等待 I/O 任务完成的同时,可以去做其他事情,提高性能。
1 | # 同步示例 |
协程(Coroutine:协同程序)
传统的函数是一步步执行到底,中途不能暂停。协程可以在 await 关键字处暂停,让出 CPU,等await任务完成后再继续执行这个协程后面的代码。这样可以在等待 I/O 任务时,不阻塞主线程,提高程序效率,在执行多个协程是一种高效的方法。
同一个协程内部是线性的!!!如果一个异步函数里面有几个await,await 只是将这个线性流程切成几段——每段之间在等待时可以让出控制权给事件循环,但当前协程不会跳过它自己的await去执行后面的代码和后面的await语句,因为在同一个函数中代码的执行必须是顺序的,不能执行到第二行碰到await任务在等待过程中去执行第三行代码,只能在等待过程中去执行另外一个协程!。
协程的定义,需要使用 async def 语句。
async def do_some_work(x): pass
1 | import asyncio # 导入 asyncio 库 |
async def my_coroutine()定义了一个协程函数,其实就是异步函数。await asyncio.sleep(2)让出 CPU,去执行其他协程,不会阻塞主线程。asyncio.run(my_coroutine())运行协程,启动事件循环。
直接调用协程函数,协程并不会开始运行,只是返回一个协程对象,要让这个协程对象运行的话,有两种方式:
1 | import asyncio |
这个过程可以理解为:将协程当做任务添加到 事件循环 的任务列表,然后事件循环检测列表中的协程任务是否已准备就绪(默认理解为就绪状态),如果准备就绪则执行协程的内部代码。
await 是一个只能在 协程函数 中使用的关键字。它的作用是当遇到 等待操作 时,挂起当前协程,让事件循环先去执行其他协程任务。直到当前任务的await执行完成后,事件循环会将控制权切换回当前任务,继续执行协程函数 await 后面的代码。
事件循环(Event Loop)
事件循环可以当做是一个while循环,这个while循环在周期性的运行并执行一些任务,在特定条件下终止循环。其实也可以理解为就是创建一个while的死循环,在特定条件下终止循环.
事件循环 是异步程序的 核心调度器,负责管理协程(Coroutine)和 任务(Task);
回调(Callback) 是 当任务完成时自动执行的函数,用于通知主程序异步任务的结果。
📌 流程
- 事件循环启动。
- 将所有的 协程转换为 Task 并调度执行。
- 等待 I/O 任务完成,期间可以执行其他任务(不会阻塞)。
- 任务完成后,事件循环 恢复暂停的协程,继续执行。
任务(task)(不明白!!)
在 asyncio 中,Task 是对协程进行调度管理的对象。Task 实际上是 asyncio 事件循环的一个抽象概念,通过 Task 可以控制协程的执行,允许它们并发运行。在底层,Task 使用事件循环调度多个协程,使得它们看起来是同时运行的。
假设say_hello()是一个协程函数,直接调用协程函数并不会执行 say_hello 里的代码,只是返回一个 协程对象,协程对象必须交给事件循环去驱动,才会真的运行。除非手动把它交给事件循环(比如 await coro、asyncio.run(coro)、或者用 create_task(coro)),否则它会一直“待机”,就好像写了一个「任务说明书」,但没人去执行。
1 | coro = say_hello(1) |
asyncio.create_task(...)会立刻把协程对象封装成 Task,并安排它在事件循环中开始执行。也就是说:一旦 create_task 被调用,这个协程就已经被“扔进跑道”,等着调度器分配 CPU 来跑了,其实就是把协程放进事件循环的队列里,“准备好运行”。但不会打断当前正在运行的协程;asyncio 不会像线程那样随时抢占。只有遇到 await(或其它让出点)时,事件循环才切到别的任务。所以create_task 之后,要等第一次 await,task 才开始真正跑。**create_task 只负责“排队”,不负责“立刻打断当前协程”。**
1 | task1 = asyncio.create_task(say_hello(1)) |
这里的内容存疑
当使用 asyncio.create_task() 创建一个任务时,它会被任务调度到事件循环中,等待事件循环来调度它的执行。如果创建了任务,但如果事件循环没有运行或者没有被 await 或通过 gather()、as_completed() 等函数显式触发,任务也会执行。
在使用asyncio.create_task(coro())为协程函数创建任务时,若没有显式调用await、asyncio.gather或asyncio.as_completed等方法来等待任务完成,任务所代表的协程函数的代码依然会执行。不过,由于事件循环没有被强制等待这些任务完成,所以它们可能在事件循环结束之前都无法完成。
使用 asyncio.create_task() 方法创建 Task,它会立即调度协程的运行并返回一个 Task 对象:
1 | import asyncio |
实现协程异步其实只需做三件事:
- .定义协程函数
- 包装协程为任务
- 建立事件循环
为什么async def main()的最后一行需要await?
1 | import asyncio |
主要有三个作用:
✅ (1) 确保任务完成
如果没有 await task,而主协程在 foo() 还没跑完时就结束了,事件循环就会关闭,foo() 可能被直接取消,不一定有机会执行完。
主协程在 await asyncio.sleep(1) 后就结束了,如果没有 await task,那么 foo() 可能没机会跑到最后一行就被丢弃。
1 | import asyncio |
✅ (2) 获取返回值
如果 foo() 有返回值,只有 await task 才能拿到它的返回值。
1 | async def foo(): |
✅ (3) 捕获异常
如果 foo() 内部抛异常,那么这个异常会存在 task 里。
只有 await task,异常才会被重新抛出到当前协程。
1 | async def foo(): |
await关键字理解
await 是 asyncio 中的关键字,它用于等待一个可等待对象(awaitable)完成执行。await 允许代码非阻塞地挂起当前任务,让出控制权给事件循环,在等待期间允许其他协程任务执行。 ,await 遇到IO操作会挂起当前协程,事件循环可以去执行其他协程任务,但不是当前协程后面的代码!
任务未完成时:await 关键字会暂停当前协程的执行,让出事件循环的控制权,以便其它协程或异步任务可以执行;
任务完成时:一旦被等待的对象完成,await 表达式将返回对象的结果,并且当前协程将继续执行;
同一个协程任务中(如main()),多个await, 会依次等待可等待对象执行完成;不同协程任务中,遇到await会交替执行。 这是之前的困惑点。是否交替任务取决于是在同一个协程任务中执行还是在事件循环中执行不同的协程任务,如果是同一个协程任务有多个await,那其实本质上还是并行任务,必须等待await中的任务完成之后才能继续执行当前任务后面的代码;如果是多协程任务,那么在等待时才能切换协程任务并发运行!
1 | # 同一任务 |
asyncio常用函数
asyncio.run(main()):运行一个顶层的协程(main()),并自动管理事件循环。适用于主程序的入口点,在该事件循环中运行main(),直到它完成。asyncio.create_task(coro()):创建一个新的并发任务(Task),用于在事件循环中并行执行协程,但不是抢占式的,只有碰到第一个await时才会开始执行这个task。
1 | import asyncio |
await asyncio.sleep(seconds):异步休眠,不会阻塞事件循环,让出控制权,允许事件循环调度其他任务执行asyncio.gather(coros(),foo()):并发运行多个协程,以列表的形式收集它们的返回值。
1 | import asyncio |
asyncio.wait(tasks):等待多个任务完成,但不像gather那样收集返回值。asyncio.as_completed():返回已完成任务的结果,按完成顺序返回,不需要等到所有任务都完成才返回
await asyncio.sleep(1) 和 time.sleep(1) 的主要区别在于它们的阻塞行为:
await asyncio.sleep(1)(非阻塞)
- 异步操作,不会阻塞事件循环。
- 让出 CPU 控制权,允许其他
asyncio任务在这 1 秒内继续执行。 - 适用于
asyncio事件循环内部的任务调度。
1 | import asyncio |
time.sleep(1)(阻塞)
- 同步操作,会完全阻塞当前线程。
- 在调用
time.sleep(1)期间,Python 解释器不会执行任何其他代码,即使是asyncio任务。 - 适用于普通同步代码,但不适用于
asyncio事件循环,因为它会让整个事件循环卡住。







