Python FastAPI异步变量共享

5
如果我有以下代码,变量service将如何影响端点的异步性?该变量是否会被共享?还是在使用时会被锁定,从而阻止其他端点访问它,直到当前端点完成为止?
我问上面的问题是假设Service实例是无状态的,即在每个端点中创建一个Service实例是等效的。我不愿意这样做,因为我不知道哪种方式更耗时,实例化和销毁Service对象还是分享一个对象?
from typing import List, Union
from fastapi import APIRouter, Body, Depends

# Service definition
router = APIRouter()
service = Service()

@router.post("/a", response_model=Union[A, None])
async def x():
    service.start()
    pass

@router.post("/b", response_model=Union[B, None])
async def y():
    service.stop()
    pass

1
“start”和“stop”函数是做什么的?如果它们执行得足够快,那么这段代码就是有效的。 - alex_noname
1个回答

7

变量服务将如何影响端点的异步性质?

首先,如果您的service.stop()不是协程,则asyncio不会进行任何上下文切换。

这意味着它将阻塞

这也意味着您的函数必须是awaitable,它必须是yielding

在使用时是否会被锁定

如果存在竞争条件,则它不会自动锁定。您需要锁定它(见asyncio.Lock())

但是,如果您的代码不执行任何上下文切换,则不需要担心这一点,因为两个协程不能同时执行,并发不等于并行

在事件循环中(协程执行的地方),协程会产生一个事件,以便恢复。事件循环能够等待这些事件发生。但是,在等待期间,它也可以等待其他事件,或者处理其他事件。这仅适用于有协程的情况下,事件通信通过事件循环的控制来完成。
但是,当两个事件共享同一个List时,需要锁定它。
为了更加清晰,想象你的代码是一家餐厅,协程就像是服务员,你向服务员下订单,然后她前往大厨(事件循环)。
你需要等待,同时你不能分享大厨,然而大厨可以同时做两个汉堡(处理两个不同的事件)。
当大厨没有足够大的灶台同时做两个汉堡时(共享对象),会发生什么呢?
当你的订单还在等待时,你需要锁定灶台以让其他客户点餐,但是你不能分享灶台,因为你自己也需要使用它,所以你需要说“嘿,我需要把这里锁住”。
但是你仍然可以点一个沙拉(其他协程不会被阻塞)。

在这种情况下,如果我在它们每个人中都执行 Service().start(),那么速度会变慢吗?

简短总结

问题完全取决于您的 start() 函数的操作方式和执行方式。

好的,但我需要更好地了解 asyncio。

那么假设您有以下代码:

async def x():
    a = await service.start()
    return a
  1. 这将为 service().start() 的 yielding 变量分配堆栈空间。
  2. 事件循环将执行此操作并跳转到下一条语句。
    1. 一旦执行 start(),它将推送调用堆栈的值。
    2. 这将存储堆栈和指令指针。
    3. 然后它将把来自 service().start() 的 yielded 变量存储到 a,然后恢复堆栈和指令指针。
  3. 当执行 return a 时,它将把变量 a 推送到调用堆栈。
  4. 最后,它将清除堆栈和指令指针。

请注意,我们能够做到这一点,是因为 service().start() 是一个 coroutine,它是 yielding 而不是返回。

这可能对您来说一开始并不清楚,但正如我提到的,asyncawait 只是声明和管理 coroutines 的花哨语法。

import asyncio

@asyncio.coroutine
def decorated(x):
    yield from x 

async def native(x):
    await x 

但这两个函数的作用完全相同。你可以想象使用yield from将一个或多个函数链接在一起。
但要深入理解异步I/O,我们需要了解它是什么以及在底层如何运行。
在大多数操作系统中,可用基本API select()poll()系统调用。
这些接口使API用户能够检查是否有任何传入的I/O需要处理。
例如,您的HTTP服务器希望检查是否有到达的网络数据包以便进行服务。通过这些系统调用,您能够进行检查。
当我们查看 select()手册页时,我们会看到以下描述。

select() 和 pselect() 允许程序监视多个文件描述符,等待其中一个或多个文件描述符变为准备好某类I/O操作(例如,输入可能)。如果可以执行对应的I/O操作,则认为文件描述符已准备就绪

这让你有一个相当基本的想法,解释了异步 I/O 的性质。
它允许您检查描述符是否可以读取和写入。
通过不阻塞其他事情,它使您的代码更具可扩展性。作为奖励,您的代码变得更快,但这不是异步 I/O 的实际目的。
所以来整理一下。 事件循环只需在准备好时继续产生,这样就不会阻塞。

那么你的意思是,如果我在多个异步函数之间共享同一个变量,它们不会自动锁定资源,因为它们是并发运行而不是并行运行?那么在这种情况下,如果我在每个函数中都执行Service().start(),会变慢吗?(因为增加了内存分配时间) - Rami Awar
1
是的,我所使用过的编程语言都没有自动锁定某些变量的功能,你需要自己进行锁定。即使它并行运行,你仍然需要锁定它。这不是特定于任何一种语言,因为在你的物理内存中有一个位置,你正在尝试更改它。这是相当实际的,那时没有抽象概念。协程只是对调用堆栈和指令指针的抽象。在堆栈中执行的每个代码片段都有一个特定的指令。 - Yagiz Degirmenci
我认为评论不够了,所以我更新了答案。 - Yagiz Degirmenci
有道理,很难自动知道要锁定哪个内存,谢谢! - Rami Awar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接