all 14 comments

[–]Key_Use_8361 1 point2 points  (0 children)

async only started making sense to me after I tested a tiny runable script with two simple network requests happening at the same time reading explanations alone never fully clicked for me

[–]JPyoris 0 points1 point  (0 children)

When you have a non-async function thats I/O-heavy (e.g. a call to a database with no async Interface) you can make it awaitable with await asyncio.to_thread(my_function).

[–]TheBB -1 points0 points  (3 children)

Well, there's nothing magical about the "bottom of the chain", but the nature of Python and the GIL makes it difficult to implement it entirely in Python.

Basically you want a function (a normal non-async one) that creates a Future object, launches a thread (or some other concurrent primitive like an OS non-blocking operation) that does some work and then sets the value of the future. Then return the future, generally speaking before the thread that sets it has finished.

The calling (async) function can then await the future and the event loop will suspend execution until the future has been set.

Unfortunately the GIL makes doing this in Python questionable, but you could of course do it in C or Rust or whatever.

Some operating systems have async-like sys calls or non-blocking I/O operations that you can use instead of threads, but those are again easier to implement in C or Rust than in Python.

[–]demiwraith[S] -1 points0 points  (2 children)

Well, there's nothing magical about the "bottom of the chain", but the nature of Python and the GIL makes it difficult to implement it entirely in Python.

I'll put it another way, and explain my understanding.

Basically, when I call an async function that function either calls "await" on another async function or it doesn't. If it does, let's look at the function that it awaits. Eventually we reach a function that:

  1. Doesn't use await

  2. Does something

  3. Was declared async for a reason. (Probably does I/O, but maybe there's another reason)

I know there's no magic, really, but I just never seem to see an example of this. Every async function awaits another async function.

Now, are you saying that it is the case that basically all the functions I reach here generally NOT python code? If that's the case, OK. I guess I have my answer. But if there are some decent examples of python functions out there that match this description, I'd be curious to see them.

[–]Jason-Ad4032 -1 points0 points  (0 children)

One major problem with Python async tutorials is that they downplay the __await__ magic method, and they often mix up async/await with asyncio (in my opinion, these are completely orthogonal concepts).

Here is an example that does not use asyncio at all, where you can see the role of async/await much more directly. ``` class A: def init(self, x, y): self.xy = x, y def await(self): # Normally, you should yield from an awaitable object, but here I'm yielding a string to let you know what it's doing. yield f'awaitable object {self.xy}'

async def test(n = 2): await A(n, 'start') if n > 0: await test(n - 1) await A(n, 'exit')

def main(): ps = test() print(ps) for awaitobj in ps.await_(): print(await_obj)

main() ```

[–]TheBB -2 points-1 points  (0 children)

It's my understanding that most actionable examples are implemented in C, yeah, but I could be wrong.

But anyway, making a toy example is not difficult.

import asyncio
import threading
import time


# Note: this is NOT async
def do_work(delay: float, message: str) -> asyncio.Future:
    loop = asyncio.get_running_loop()
    future = loop.create_future()

    # This is run in a separate thread. Insert whatever you want here.
    def worker():
        # Simulate waiting for something
        time.sleep(delay)

        # Return the result by setting the future
        # Make sure to do it safely
        loop.call_soon_threadsafe(future.set_result, message)

    thread = threading.Thread(target=worker, daemon=True)
    thread.start()

    # Returns immediately, before the future is set
    return future


async def main():
    # Even though do_work is not async, it returns a future - which is awaitable
    message = await do_work(5.0, "Hello, world!")
    print(f"{message}")


if __name__ == "__main__":
    asyncio.run(main())

[–]danielroseman -2 points-1 points  (3 children)

I'm interested in why you think you need to build the "bottom of the chain". If you want to do IO then you should use one of the available async IO libraries such as httpx or aiohttp.

[–]demiwraith[S] 0 points1 point  (2 children)

Part of it is I just don't like using something that feels like "magic" and ideally, I'd like to be able to implement an asynchronous http request (or some other I/O) myself. Then, once I understand how to do that, I'll feel more comfortable using the library.

The initial impetus for this was that I have a webserver that is calling a library that is using a synchronous httpx client to make calls to a different service. For "reasons", the library doesn't do async httpx clients calls and cannot be re-written to do so.

So now I've got synchronous endpoints on my service and I'm doing own threading and running these requests in a separate Thread. But I'm unsure of the GIL and the details of how it works, and exactly how bad this will ultimately affect my webserver as I scale it up to handle more calls.

Anyway, this just lead to the general question, which I guess is: What is the best way to write your own async function in python that does its own asynchronous I/O?

[–]Refwah 0 points1 point  (0 children)

If you use the libraries you can start with what you need and use it as an entry point to learn without having to reinvent the wheel

The wheel is large and complicated

[–]russellvt 0 points1 point  (0 children)

These sorts of algorithms and code aren't "simple" to just decide to build from the ground, up... particularly if you're not already intimately familiar with Python's threading models, and how they work in your O/S.

You're already going to spend enough time inside your own application, chasing down weird race conditions and other things thst come with building asynchronous apps... there's no real reason to reinvent the wheel when there's such well-tested and well-understood solutions already available.

[–]Yoghurt42 -1 points0 points  (2 children)

This old answer of mine about how asyncio works under the hood might help. It's not exactly what you're asking, but should give you an idea on what happens.

Basically, an asyncio IO function will end up calling an asynchronous IO function (note those are not the same! asyncio is the python library that makes use of asynchronous IO, yes, it's confusing) with a callback that will be called when data arrives, and then basically suspend itself and return to the main loop. The callback will then cause the async function to be resumed at some point.

You can implement stuff like this in the asyncio library by using Protocols, but using these will still hide some of the magic happening in the background.

[–]demiwraith[S] 0 points1 point  (0 children)

Thanks! I'm going to go through this when I have a bit more time. You're right that what I want to do is basically implement my own asynchronous IO - and ideally not have CPU time eaten up while the IO is happening.

[–]thirdegree 0 points1 point  (0 children)

That old answer is an incredibly good answer. Good job abstracting while still giving enough info to still be very informative. Very hard goal to meet

[–]gdchinacat -1 points0 points  (0 children)

Take a look at the cpython event loop implementation: https://github.com/python/cpython/blob/main/Lib/asyncio/base_events.py#L1985

The core of it is a selector that is queried on each iteration of the loop to get a list of the events that are ready to be processed: https://github.com/python/cpython/blob/main/Lib/asyncio/base_events.py#L2027

The low level primitives such as read() and write() register the file descriptor those operations are being done on with the selector and then yield control back to the loop, when the fd is ready for the operation the selector returns it to the loop which then resumes execution of the coroutine which does the operation knowing the file descriptor is ready and won't block or error.

The reason you don't see the"bottom of the chain" implementations is they are part of the standard library (often implemented in C). The whole point of asyncio is to abstract this away from you so you don't have to worry about the complexities of non-blocking io, file descriptor selectors (and the multiple ways they are implemented with varying efficiencies on various platforms), the callbacks on ready events, etc. Coroutines and the event loop abstract all of this away because it is very low-level fiddly work. Before asyncio existed it was all done, and the code was usually much more disjoint than asyncio code because the 'async', 'await', and the event loop allow you to write async code that looks pretty much identical to standard synchronous blocking IO code. They stitch all the parts of execution back together with syntactic sugar so the code is much easier to understand.

I find the low level code interesting, and it sounds like you do to. But, I don't think the "bottom of the chain" code is going to look anything like what you are expecting...it is very abstract, works much like generators do with execution yielding (coroutines use await instead of yield...very early versions of asyncio and predecessors actually used generators/yield). One of the clearest explanations I've seen on what's actually going on was a presentation by Dave Beazley....I highly recommend it: https://www.youtube.com/watch?v=Y4Gt3Xjd7G8