深入解析:Python异步(Asyncio)(一)
在当今高并发、高性能计算的时代,传统的同步编程模型在面对I/O密集型任务时常常显得力不从心。当我们的应用程序需要同时处理成百上千的网络连接、文件操作或数据库查询时,线程和进程的开销往往成为性能瓶颈。正是在这样的背景下,异步编程范式应运而生,而Python的asyncio模块则为我们提供了一个优雅且高效的解决方案。
asyncio不仅仅是Python标准库中的一个模块,它代表了一种全新的编程思维——通过协程和事件循环实现轻量级并发,让单线程程序也能高效处理大量并发任务。这种基于协作式多任务处理的模型,相比传统的抢占式多任务(线程/进程),在资源消耗和上下文切换开销上具有显著优势。本文将深入探讨asyncio的核心概念、工作原理和实践应用,帮助开发者掌握这一强大的异步编程工具,构建更高效、更响应迅速的应用程序。
异步编程
异步编程是一种不会阻塞的编程范式。
异步任务
异步意味着不同时,与同步或同时相对。
在编程中,异步意味着操作被请求,尽管不是在请求时执行。它会在稍后执行。
异步函数调用:请求在某个时间以某种方式调用函数,允许调用者继续执行其他活动。
函数调用会在某个时间以某种方式在后台进行,程序可以执行其他任务或响应其他事件。这是关键。我们无法控制请求的处理方式或时间,只是希望它在程序执行其他任务时得到处理。
发出异步函数调用通常会产生对请求的某种控制权,调用者可以使用它来检查调用状态或获取结果。这通常被称为 future。
Future:一个异步函数调用的句柄,允许检查调用状态并检索结果。
异步函数调用和 Future 的结合通常被称为异步任务。这是因为它比函数调用更复杂,例如允许取消请求等。
异步任务:用于指代异步函数调用及其结果 Future 的集合。
异步编程
发起异步任务和进行异步函数调用被称为异步编程。
异步编程:意味着一个特定的长时间运行的任务可以在主应用程序之外在后台运行。系统不会阻塞所有其他应用程序代码等待该长时间运行的任务完成,而是可以去做不依赖于该任务的其他工作。然后,一旦长时间运行的任务完成,系统会通知我们已完成,以便我们可以处理结果。
异步编程主要用于非阻塞 I/O,例如从与其他进程或系统的套接字连接中读取和写入。
非阻塞 I/O 是一种执行 I/O 的方式,其中读取和写入操作被请求,尽管它们是异步执行的。调用者无需等待操作完成即可返回。
非阻塞 I/O 通过异步请求和响应执行 I/O 操作,而不是等待操作完成。
读和写操作以某种方式执行(例如,由底层操作系统或基于其构建的系统执行),并且调用者在操作状态和数据可用时,或调用者准备就绪时,稍后检索该状态和数据。
因此,我们可以看到非阻塞 I/O 与异步编程的关系。事实上,我们通过异步编程使用非阻塞 I/O,或者非阻塞 I/O 是通过异步编程实现的。
非阻塞 I/O 与异步编程的结合非常普遍,因此通常简称为异步 I/O。
Python 中的异步编程
广义上讲,Python 中的异步编程是指发起请求但不阻塞以等待其完成。
asyncio 模块。该模块直接提供了使用 async/await 语法和非阻塞 I/O(通过套接字和子进程)的异步编程环境。
asyncio 是异步 I/O 的缩写。它是一个 Python 库,允许我们使用异步编程模型运行代码。这使我们能够同时处理多个 I/O 操作,同时仍然允许我们的应用程序保持响应。
它使用协程实现,这些协程运行在一个事件循环中,而该事件循环本身在单个线程中运行。
- Asyncio:Python 通过 asyncio 模块提供的异步编程环境。
Python 提供了线程和进程,可以异步执行任务
更具体地说,Python 在 ThreadPoolExecutor 和 ProcessPoolExeuctor 类中提供了基于执行器的线程池和进程池。
这些类使用相同的接口,并通过返回 Future 对象的 submit()方法支持异步任务。
concurrent.futures 模块提供了用于异步执行可调用对象的接口。异步执行可以通过线程(使用 ThreadPoolExecutor)或单独的进程(使用 ProcessPoolExecutor)来完成。
multiprocessing 模块也通过 Pool 和 ThreadPool 类使用进程和线程提供工作池,它们是 ThreadPoolExecutor 和 ProcessPoolExeuctor 类的前身。
这些类的功能以异步方式描述了工作执行任务。它们明确为每个方法提供了同步(阻塞)和异步(非阻塞)的版本来执行任务。
什么是 Asyncio
广义上讲,asyncio 指的是使用协程在 Python 中实现异步编程的能力。
Python 语言通过添加表达式和类型进行了修改,以适应 asyncio。
对 Python 的更改以添加对协程的支持
Python 语言通过添加表达式和类型进行了修改,以适应 asyncio。
更具体地说,它被修改为支持作为一等概念的使用协程。反过来,协程是 asyncio 程序中使用的并发单元。
协程是一个可以挂起和恢复的函数。
# 协程可以通过“async def”表达式来定义。它和函数一样可以接收参数并返回值。
async def custom_coro():
# ...
# 调用协程函数会创建一个协程对象,这是一个新的类。它不会执行协程函数。
# create a coroutine object
coro = custom_coro()
# 协程可以通过 await 表达式执行另一个协程。这会挂起调用者并将目标安排执行。
await custom_coro()
asyncio 模块
“asyncio”模块提供了用于使用异步编程范式开发基于协程的程序的功能和对象。
具体来说,它支持使用子进程(用于执行命令)和流(用于 TCP 套接字编程)的非阻塞 I/O。
asyncio 是一个使用 async/await 语法编写并发代码的库。
asyncio 模块的核心是事件循环。这是运行基于协程的程序并实现协程间协作多任务的机制。
事件循环是每个 asyncio 应用程序的核心。事件循环运行异步任务和回调,执行网络 IO 操作,并运行子进程。
该模块提供了高层和低层 API。
何时使用 Asyncio
使用 Python asyncio 的理由
- 在程序中使用asyncio来使用协程
- 使用 asyncio 来使用异步编程范式
- 使用 asyncio 以便使用非阻塞 I/O
使用协程
协程是另一种并发单元,类似于线程和进程。
基于线程的并发由 threading 模块提供,并得到底层操作系统的支持。它适用于阻塞 I/O 任务,例如从文件、套接字和设备中进行读写。
基于进程的并发由 multiprocessing 模块提供,并且也得到底层操作系统的支持,类似于线程。它适用于不需要大量进程间通信的 CPU 密集型任务,例如计算任务。
协程是由 Python 语言和运行时(标准解释器)提供的一种替代方案,并且得到了 asyncio 模块的进一步支持。它们适用于非阻塞 I/O、子进程和套接字,但通过使用线程和进程,阻塞 I/O 和 CPU 密集型任务也可以以模拟的非阻塞方式使用。
线程和进程通过操作系统实现多任务处理,操作系统决定哪些线程和进程应该运行、何时运行以及运行多长时间。操作系统快速地在线程和进程之间切换,挂起那些未运行的线程和进程,并恢复那些获得运行时间的线程和进程。这被称为抢占式多任务处理。
Python 中的协程提供了一种名为协作式多任务处理的替代型多任务方式。
协程是一种可以暂停和恢复的子程序(函数)。它通过 await 表达式暂停,并在 await 表达式解决后恢复执行。这使得协程能够按设计协同工作,选择何时以及如何暂停它们的执行。
这是一种不同于基于线程和基于进程的并发方法,是一种替代的、有趣的且强大的并发方式。
协程的另一个关键方面是它们轻量级。它们比线程更轻量级。这意味着它们启动更快,使用更少的内存。本质上,协程是一种特殊类型的函数,而线程由一个 Python 对象表示,并与操作系统中的线程相关联,对象必须与之交互。
因此,在 Python 程序中我们可能有成千上万的线程,但我们可以轻易地在单个线程中拥有数十万甚至上百万个协程。
使用异步编程
异步意味着不同时,与同步或同时相对。
在编程中,异步意味着请求行动时,行动并不立即执行,而是在稍后执行。
异步编程通常意味着全力以赴,围绕异步函数调用和任务的概念来设计程序。
在 Python 中进行完整的异步编程需要使用协程和 asyncio 模块。
异步编程可以独立于非阻塞 I/O 使用。
协程可以异步执行非阻塞 I/O,但 asyncio 模块还提供了以异步方式执行阻塞 I/O 和 CPU 密集型任务的功能,通过线程和进程在底层模拟非阻塞操作。
使用非阻塞 I/O
输入(input)/输出(output)简称为 I/O,是指从资源中读取或写入数据。常见的例子有:
- 硬盘驱动器:读取、写入、追加、重命名、删除等文件。
- 外设:鼠标、键盘、屏幕、打印机、串口、摄像头等。
- 互联网:下载和上传文件、获取网页、查询 RSS 等。
- 数据库:选择、更新、删除等。SQL 查询。
与使用 CPU 计算相比,这些操作很慢。这些操作在程序中的常见实现方式是发起读或写请求,然后等待数据发送或接收。因此,这些操作通常被称为阻塞 I/O 任务。
操作系统可以看到调用线程被阻塞,并将上下文切换到另一个将使用 CPU 的线程。这意味着阻塞调用不会减慢整个系统。但它确实会暂停或阻塞进行阻塞调用的线程或程序。
非阻塞 I/O 是阻塞 I/O 的替代方案。它需要底层操作系统提供支持,就像阻塞 I/O 一样,所有现代操作系统都为某种形式的非阻塞 I/O 提供了支持。
非阻塞 I/O 允许将读和写调用作为异步请求进行。操作系统将处理该请求,并在结果可用时通知调用程序。
非阻塞 I/O:通过异步请求和响应执行 I/O 操作,而不是等待操作完成。
非阻塞 I/O 与异步编程的关系:通过异步编程使用非阻塞 I/O,或者非阻塞 I/O 是通过异步编程实现的。
非阻塞 I/O 与异步编程的结合,通常简称为异步 I/O。
异步 I/O:一个简称,指的是将异步编程与非阻塞 I/O 相结合。
阻塞调用是指函数调用在完成之前不会返回。所有普通函数都是阻塞调用。
在并发编程中,阻塞调用是指那些会等待特定条件并通知操作系统在线程等待期间没有发生事情的函数调用。
操作系统可能会注意到一个线程正在执行阻塞函数调用,并决定切换到另一个线程的上下文。正在运行的线程被挂起,挂起的线程被恢复并继续运行。线程的挂起和恢复称为上下文切换。
操作系统倾向于从阻塞线程中切换上下文,允许非阻塞线程运行。
这意味着如果线程执行了一个阻塞函数调用,一个等待的调用,那么它可能会发出信号表示该线程可以被挂起,从而允许其他线程运行。
asyncio 模块通过协程、事件循环以及用于表示非阻塞子进程和流的对象,为非阻塞 I/O 提供了一流异步编程支持。
Python 中的协程
Python 提供了一流的协程,具有“coroutine”类型和新的表达式如“async def”和“await”。它提供了“asyncio”模块用于运行协程和开发异步程序。
什么是协程(coroutine)
协程是一个可以挂起和恢复的函数。它通常被定义为一个通用的子程序。
一个子程序可以从一点执行到另一点完成。而协程可以执行后暂停,并在多次恢复后最终终止。
许多协程可以同时创建和执行。它们控制自己何时暂停和恢复,从而能够协作决定并发任务何时执行。这被称为协作式多任务处理,与线程通常使用的抢占式多任务处理不同。
抢占式多任务处理涉及操作系统选择要挂起和恢复哪些线程以及何时进行,而协作式多任务处理中,任务本身决定何时让出控制权。
协程(coroutine)与例程(routine)和子例程(subroutine)
“routine”和“subroutine”通常指的是同一事物,更准确地说,一个例程是一个程序,而一个子例程是程序中的一个函数。
子程序(subroutine):一个可以在需要时执行的指令模块,通常有名称,可以接收参数并返回值。也称为函数。
一个子程序被执行,运行通过表达式,并以某种方式返回。通常,一个子程序是由另一个子程序调用的。协程是子程序的扩展。这意味着子程序是一种特殊的协程。
协程(coroutine)与生成器(generator)
生成器是一种特殊的函数,可以挂起其执行。
生成器:一种返回生成器迭代器的函数。它看起来像是一个普通函数,但它包含 yield 表达式,用于产生一系列可在 for 循环中使用的值,或者可以通过 next()函数逐个获取。
生成器函数可以像普通函数一样定义,尽管它在将要挂起执行并返回值的地方使用 yield 表达式。
生成器函数将返回一个可遍历的生成器迭代器对象,例如通过 for 循环。每次执行生成器时,它将从上次暂停的位置运行到下一个 yield 语句。
生成器迭代器:由生成器函数创建的对象。每次调用 yield 时,会暂时挂起处理,记住执行状态(包括局部变量和待处理的 try 语句)。当生成器迭代器恢复执行时,它会从上次暂停的地方继续(与每次调用时都从头开始执行的标准函数不同)。
协程可以通过“await”表达式挂起或让出执行权给另一个协程。一旦被等待的协程执行完成,它将从这一点继续执行。
await 语句在功能上类似于 yield 语句;当运行其他代码时,当前函数的执行会被暂停。一旦 await 或 yield 解析出数据,函数就会继续执行。
我们可以将生成器视为一种在循环中使用的特殊协程和协作式多任务处理类型。
协程(coroutine)与任务(task)
子程序和协程都可能代表程序中的一个“任务”。
在 Python 中,有一个特定的对象称为 asyncio.Task 对象。
一个运行 Python 协程的 Future 类对象。 […] Tasks 用于在事件循环中运行协程。
协程可以包装在 asyncio.Task 对象中并独立执行,而不是直接在协程内执行。Task 对象提供了异步执行协程的句柄。
这允许被包装的协程在后台执行。调用协程可以继续执行指令,而不是等待另一个协程。一个Task不能独立存在,它必须包装一个协程。因此,一个 Task 是一个协程,但一个协程不是一个 Task。
协程(coroutine)与线程(thread)
协程比线程更轻量级。协程被定义为函数。
线程是一个由底层操作系统创建和管理的对象,在 Python 中表现为 threading.Thread 对象。
这意味着协程通常创建和开始执行的速度更快,并且占用的内存更少。相反,线程创建和开始执行的速度比协程慢,并且占用更多的内存。
协程在单个线程中执行,因此单个线程可以执行多个协程。
协程(coroutine)与进程(process)
协程比进程更轻量级。
一个进程是一个计算机程序。它可能有一个或多个线程。一个 Python 进程实际上是一个独立的 Python 解释器实例。
进程,如同线程,是由底层操作系统创建和管理的,并以 multiprocessing.Process 对象的形式表示。
这意味着协程在创建和启动方面比进程要快得多,并且占用的内存也少得多。
协程只是一个特殊函数,而进程是解释器的一个实例,它至少有一个线程。
定义、创建和运行协程
“asyncio”模块提供了工具,用于在事件循环中运行我们的协程对象,事件循环是协程的运行时环境。
定义一个协程
协程可以通过“async def”表达式来定义。它定义了一个协程,可以创建并返回一个协程对象。使用“async def”表达式定义的协程被称为“协程函数”。
协程函数:返回协程对象的函数。协程函数可以用 async def 语句定义,并且可以包含 await、async for 和 async with 关键字。
创建协程
# 检查协程的类型
# 定义一个协程
async def custom_coro():
# 等待另一个协程
await asyncio.sleep(1)
# 创建协程
coro = custom_coro()
# 检查协程的类型
print(type(coro))
:10: RuntimeWarning: coroutine 'custom_coro' was never awaited
coro = custom_coro()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
运行示例报告称,创建的协程是一个“coroutine”类。
同时出现RuntimeError,因为协程被创建了但从未被执行
运行协程
协程可以定义和创建,但它们只能在事件循环中执行。
事件循环(event loop)是每个 asyncio 应用程序的核心。事件循环运行异步任务和回调,执行网络 IO 操作,并运行子进程。
执行协程的事件循环,管理协程之间的协作式多任务处理。
启动协程事件循环的典型方式是通过 asyncio.run()函数。
这个函数接受一个协程并返回协程的值。提供的协程可以用作基于协程的程序入口点。
# SuperFastPython.com
# 运行协程的示例
import asyncio
# 定义一个协程
async def custom_coro():
# 等待另一个协程
await asyncio.sleep(1)
# 主协程
async def main():
# 执行自定义协程
await custom_coro()
# 启动协程程序
asyncio.run(main())
事件循环
asyncio 程序的精髓是事件循环。
Asyncio 事件循环
事件循环是一个在单个线程中执行协程的环境。
asyncio 是一个库,它使用一种称为单线程事件循环的并发模型,以异步方式执行这些协程。
事件循环是 asyncio 程序的核心。
- 执行协程
- 执行毁掉函数
- 执行网络输入、输出
- 运行子进程
事件循环顾名思义是一个循环。它管理一个任务(协程)列表,并在每次循环迭代中依次尝试推进每个任务,同时执行其他任务,如执行回调和处理 I/O。
“asyncio”模块提供了用于访问和交互事件循环的函数。
启动和获取事件循环
在 asyncio 应用程序中,我们通常通过 asyncio.run()函数来创建事件循环。
该函数接收一个协程,并执行至完成。通常将其传递给主协程,并从那里运行程序。
asyncio.new_event_loop() 函数将创建一个新的事件循环并返回对其的访问权限。
# 创建事件循环的示例
import asyncio
# 创建并访问一个新的asyncio事件循环
loop = asyncio.new_event_loop()
# 报告循环的默认信息
print(loop)
# 获取事件
print(asyncio.get_event_loop())
print(asyncio.get_running_loop())
<_UnixSelectorEventLoop running=False closed=False debug=False>
我们可以看到在这种情况下,事件循环的类型是 _UnixSelectorEventLoop,它没有运行,但也未关闭。
事件循环对象
事件循环被实现为一个 Python 对象。事件循环对象定义了事件循环的实现方式,并提供了一个用于与循环交互的通用 API,该 API 在 AbstractEventLoop 类中定义。
不同的平台有不同的事件循环实现。
为什么要获取事件循环的访问权限
为什么我们需要在 asyncio 程序之外访问事件循环?
- 用于监控任务进度
- 用于从任务中发出和获取结果
- 用于触发并忘记一次性任务
异步事件循环可以用作程序中基于协程任务的线程池替代方案。
事件循环也可以嵌入到普通的 asyncio 程序中,并根据需要访问。
结语
掌握asyncio并非一蹴而就。它要求我们转变传统的编程思维,理解协作式多任务的本质,学会在适当的场景选择合适的并发模型。在实践中,我们应避免过度使用asyncio处理CPU密集型任务,而是将其优势发挥在I/O密集型场景中;同时,也要注意与现有同步代码的集成策略,避免出现"回调地狱"或复杂的异步/同步混合代码。这篇文章作为对asyncio研究的第一篇,我们可以知道在python中的异步是什么以及何时使用它,后续会慢慢更新,敬请期待。
Reference
- https://superfastpython.com/python-asyncio/
浙公网安备 33010602011771号