同步与异步

Litestar 在几乎所有可能的地方都支持同步和异步可调用对象。一般来说,支持三种不同的执行模式:

  • 直接运行异步可调用对象

  • 直接运行同步可调用对象

  • 在线程池中运行同步可调用对象

本文概述了这些模式之间的重要区别。

阻塞和非阻塞

在异步编程的上下文中,术语 阻塞非阻塞 通常用于描述函数的特定质量:阻塞执行流程。

异步函数本身并不是非阻塞的。它们所做的是允许程序员精确控制 在哪里 解除阻塞并将控制权返回给主循环,允许其他异步任务运行,这通常由 await 关键字的使用来指示。这是一个非常重要的方面需要注意,因为从不调用 await 并且例如执行计算密集型任务的异步函数 在其整个运行时阻塞主线程,就像同步函数一样。更重要的是,异步函数内部在等待部分 之间 发生的任何事情也将被阻塞。

从技术上讲,这意味着 Python 中没有非阻塞函数,因为异步函数只在每个 await 处"解除阻塞"。由于这不是该术语的有用定义,"阻塞"通常是指 长时间阻塞 的可调用对象。

备注

由于"阻塞"是关于执行流程的,人们可能会认为 await 也是阻塞的;执行不会继续进行,直到可等待对象被解析,这符合该术语的定义。但是,由于与此同时 *程序的其他部分*(更准确地说,其他在 await 处放弃控制并已完成的协程)被允许继续,这不被认为是阻塞的,通常称为"等待"。

I/O 密集型与 CPU 密集型

在尝试确定函数是否应该是异步时要考虑的另一个重要方面是它 为什么 可能阻塞。一般来说,阻塞操作有两种不同的类别:

I/O 密集型

对文件系统或网络的调用是 I/O 密集型阻塞的典型示例。它们的执行速度在很大程度上受到外部资源完成所需时间的限制,这使得它们"绑定"到该资源。

它们非常适合异步,因为大部分时间都花在等待这些操作完成上。这意味着在此期间可以执行其他任务,"并行等待",大大减少了总体运行时间。

CPU 密集型

不等待 I/O 的操作通常可以被视为 CPU 密集型。由于它们不等待外部资源,它们的执行速度受 CPU 限制,即它可以多快执行给定的指令。

它们不像 I/O 密集型任务那样从异步执行中受益,因为它们不会花费大量时间等待。

异步 CPU 密集型任务

在某些情况下,可以通过引入事件循环切换到其他任务的点来使 CPU 密集型任务异步。一个例子是在每次迭代开始时等待 asyncio.sleep(0) 的内部循环。

这种技术主要用于长时间运行的任务,其中每个单独的步骤不会长时间阻塞。

何时使用异步函数

当异步函数可以从并发执行中受益时应该使用它们,也就是说,它们本身执行异步操作,例如调用其他异步函数或异步迭代,并且不执行任何阻塞操作,例如调用 I/O 密集型的同步函数。

为什么不默认使用 async?

可能很诱人地看待这一点并认为函数应该默认为 async,除非它同步执行阻塞操作,但事实并非如此。Async 本身有一个附加的开销,虽然非常小,但在某些情况下可能会变得不可忽略。执行非阻塞操作的同步函数将优于执行相同操作的异步函数。

何时使用同步函数

作为前一段的反面,同步函数应该用于非 I/O 密集型任务。同步执行模型允许最小的开销量,因此应该在不使用异步功能的情况下首选。

何时使用线程池

如果函数执行计算密集或 I/O 密集型操作,无法被异步等效项替换,则可以使用 sync_to_thread=True 在线程池中运行它。

为什么不将此作为同步函数的默认设置?

在线程池中运行具有非常高的开销,大大降低了应用程序的性能。因此,其使用应仅限于绝对必要的情况。

限制

虽然在线程池中运行 CPU 密集型函数确实允许以非阻塞方式运行它,但它不会加快其执行速度。定期执行的计算密集型任务应该卸载到不同的进程中,以利用多个 CPU 核心。

这可以通过使用 anyio.to_process.run_sync() 来实现。

关于执行模式的警告

由于同步函数可能是阻塞的,Litestar 会在可能阻塞主事件循环并影响应用程序性能的地方对其使用发出警告。如果同步函数是非阻塞的,设置 sync_to_thread=False 将告诉 Litestar 该函数可以被视为这样。

引入此警告是为了防止意外使用阻塞函数,通过必须做出关于是否在线程池中运行函数的深思熟虑的决定。

可以通过设置环境变量 LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0 来全局禁用该警告。