Python FastAPI Async Variable Sharing

爱⌒轻易说出口 提交于 2021-02-07 10:10:30

问题


If I had the below code, how would the variable service affect the asynchronous nature of the endpoints? Will the variable be shared? Or will it be locked when in use, thus blocking other endpoints from accessing it until the current one is done?

I ask the above assuming that Service instances are stateless, i.e. it would be equivalent if I created an instance of Service in each endpoint. I am reluctant to do that because I don't know which is more time consuming, instantiating and destroying a Service object or sharing one?

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:


How would the variable service affect the asynchronous nature of the endpoints?

First, if your service.stop() is not a coroutine asyncio does not do any context switching.

This means it is going to block.

This also means your function must be awaitable, it must be yielding.

Or will it be locked when in use

It does not lock automatically if there are race conditions possible. You need to lock it (see asyncio.Lock()).

But if your code does not do any context switching. You do not need to worry about that because two coroutines can not be executed at the same time, concurrency is not parallelism.

In event loops (where coroutines are executed), a coroutine yields an event on which it wants to resume. Event loop is capable of waiting for these events to happen. But while waiting for that it can also wait for other events, or it can process other events. This only works when you have a coroutine, the event communication is done by the event loop's yielding control.

But what you should do when two events are sharing the same List, you need to Lock it.


To make this more clear, imagine your code is a restaurant and the coroutine is the waitress, you give orders to the waitress, then she heads to the chef (event loop)

You need to wait, also you can not share the chief, while the chief can make two hamburgers at the same time (processing two different events)

What happens when he doesn't have a grid big enough to make two hamburgers at the same time (shared objects)?

While your order is waiting you need to lock the grid two make other customers order, but you can not share the grid because you have one and you need to say "hey i need to lock here down"

But you can still have a salad (other coroutines doesn't get blocked).


So in this case, would it be slower if I did Service().start() in each of them?

TL;DR

The question completely depends on what your start() function does and how it does.

Okay, but i need to understand asyncio better.

Then assume you have the following code

async def x():
    a = await service.start()
    return a
  1. This will allocate the stack space for the yielding variable of service().start()
  2. The event loop will execute this and jump to the next statement
    1. once start() get's executed it will push the value of the calling stack
    2. This will store the stack and the instruction pointer.
    3. Then it will store the yielded variable from service().start() to a, then it will restore the stack and the instruction pointer.
  3. When it comes to return a this will push the value of a to calling stack.
  4. After all it will clear the stack and the instruction pointer.

Note that we were able to do all this because service().start() is a coroutine, it is yielding instead of returning.

This may not be clear to you at first glance but as I mentioned async and await are just fancy syntax for declaring and managing coroutines.

import asyncio

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

async def native(x):
    await x 

But these two function are identical does the exact same thing. You can think of yield from chains one and more functions together.

But to understand asynchronous I/O deeply we need to have an understanding of what it does and how it does underneath.

In most operating systems, a basic API is available with select() or poll() system calls.

These interfaces enable the user of the API to check whether there is any incoming I/O that should be attended to.

For example, your HTTP server wants to check whether any network packets have arrived in order to service them. With the help of this system calls you are able to check this.

When we check the manual page of select() we will see this description.

select() and pselect() allow a program to monitor multiple file de‐ scriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corre‐ sponding I/O operation

This gives you a pretty basic idea, and this explains the nature of what asynchronous I/O does.

It lets you check whether descriptors can be read and can be written.

It makes your code more scalable, by not blocking other things. Your code becomes faster as a bonus, but it is not the actual purpose of asynchronous I/O.

So to tidy up.

The event loop just keeps yielding, while something is ready. By doing that it does not block.



来源:https://stackoverflow.com/questions/65410635/python-fastapi-async-variable-sharing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!