2

I'm currently trying to do something like this:

import asyncio

class Dummy:
    def method(self):
        return 1
    def __str__(self):
        return "THIS IS A DUMMY CLASS"

async def start_doing():
    asyncio.sleep(1)
    return Dummy


async def do_something():
    return start_doing().method()


async def main():
    a = asyncio.create_task(do_something())
    b = asyncio.create_task(do_something())

    results = await asyncio.gather(a, b)
    print(results)

asyncio.run(main())

But I get this error:

AttributeError: 'coroutine' object has no attribute 'method'

Which indicates that I cannot call my method on a coroutine object. One way to fix this is by doing these:

async def do_something():
    return (await start_doing()).method()

But I think that by doing this you are inherently making your code synchronous. You are not generating a future, rather waiting for your work to be finished in do_something and then proceed to the next item in the queue.

How should I call an object method in the future when the awaitable has been awaited and my object is ready? how to schedule it to be called in the future?

cglacet
  • 8,873
  • 4
  • 45
  • 60
Farhood ET
  • 1,432
  • 15
  • 32

2 Answers2

2

Await the call then call the method on an instance of the returned class.

async def start_doing():
    await asyncio.sleep(1)
    return Dummy


async def do_something():
    thing = await start_doing()
    return thing().method()

How should I call an object method in the future when the awaitable has been awaited and my object is ready? how to schedule it to be called in the future?

See if I can get this correct.

  • When you create the task do_something it is scheduled.
  • Eventually the event loop decides to let do_something start.
  • do_something calls start_doing and awaits it.
  • While do_something is waiting the event loop takes control away from it and lets something else have a turn.
  • At some points in time start_doing is scheduled, started, finishes waiting/sleeping, returns the object
    • during the waiting/sleeping part control may have been transferred to other tasks by the event loop
  • Eventually after do_something is done waiting the event loop gives control/focus back to it.
    • implies that other scheduled tasks have finished or are waiting on something.

I really don't know what the scheduling algorithm is, I think of it as a round-robin affair but it might be more intelligent than that.

wwii
  • 23,232
  • 7
  • 37
  • 77
  • Wouldn't using `await` make the coroutine wait for the result of called function and as a result run it in a blocking manner? – Farhood ET Jun 28 '20 at 16:44
  • Note that you could also write `(await start_doing())().method()` (but I still prefer the more verbose approach proposed by this answer). – cglacet Jun 28 '20 at 16:44
  • No, using `await ` is what makes it non blocking. When you call `await x` you simply say "I'm waiting for `x` feel free to schedule something else". – cglacet Jun 28 '20 at 16:45
  • @cglacet - I rolled back your edit - returning an instance from `start_doing` would have broken `do_something`. – wwii Jun 28 '20 at 16:47
  • Yes, sorry I just saw that, that just looked weird, and my comment doesn't make sense in that scenario either :D. Ah, I've been able to edit, just in time. – cglacet Jun 28 '20 at 16:47
  • .. Don't be sorry - feel free to edit my answers if you see something that needs it - I always get the chance to review. – wwii Jun 28 '20 at 16:49
  • @FarhoodET - see if my edit addresses your question. – wwii Jun 28 '20 at 17:06
  • *I think - when start_doing is called the event loop schedules it because it is a coroutine.* - this part is incorrect, the awaited coroutine gets executed immediately without going through an event loop cycle. It is only if a coroutine in the await chain chooses to *suspend* that the whole call chain is suspended and the event loop gets a chance to run. (The assumption that `await` always drops to the event loop sometimes causes bugs, as [here](https://stackoverflow.com/a/48816319/1600898).) – user4815162342 Jun 28 '20 at 20:25
  • Thank you @user4815162342 - so in this case the awaited coroutine executes immediately when it is called then because **it** awaits something then it's caller also has to wait and control drops back to the event loop.? Nice link. Is that buried in the docs someplace or inferred from a bunch of stuff in the docs? – wwii Jun 28 '20 at 23:06
  • ..From [docs](https://docs.python.org/3/library/asyncio-dev.html#concurrency-and-multithreading) - `While a Task is running in the event loop, no other Tasks can run in the same thread. When a Task executes an await expression, the running Task gets suspended, and the event loop executes the next Task.` - The Next Task must then be the called coroutine being awaited..? – wwii Jun 29 '20 at 00:04
  • @wwii The task docs are imprecise here, sadly. The running task does get suspended, but only if the awaited expression actually *suspends*. If it can provide the data immediately, it does so, and the task doesn't drop to the event loop. (The `busy_loop` example proves it, as does the issue in the original question.) It should probably be reported as a doc issue. – user4815162342 Jun 29 '20 at 13:42
  • @user4815162342 - last one I promise - 1) the awaited expression, or an *await chain* is all wrapped in the same Task? 2) It is the Task that cedes control to the event loop?. I'm going to comment on https://bugs.python.org/issue39085. – wwii Jun 29 '20 at 15:26
  • A task is how asyncio runs a coroutine. So yes, if a coroutine awaits another coroutine, that is still the same task. (And that other coroutine can in turn await another one, and so on; this stack of awaits is what I referred to as an await chain.) The bottom-most coroutine suspending will automatically suspend the whole task. Take a look at [this lecture](https://youtu.be/7JtNiwCH_OA) for a more in-depth explanation of await. – user4815162342 Jun 29 '20 at 15:36
1

To further extend on @wwii's answer and address the concerns about risk of blocking using await you can play with functions like the following f:

import time
from datetime import datetime
import asyncio

start = datetime.now()

async def f(x, block_for=3):
    print(f"{x} IN     {ellapsed_time()}s")
    time.sleep(block_for)
    print(f"{x} AWAIT  {ellapsed_time()}s")
    await asyncio.sleep(x)
    print(f"{x} OUT    {ellapsed_time()}s")
    
def ellapsed_time():
    return (datetime.now() - start).seconds
    

asyncio.create_task(f(2))
asyncio.create_task(f(1))

Which produces:

2 IN     0s
2 AWAIT  3s
1 IN     3s
1 AWAIT  6s
2 OUT    6s
1 OUT    7s

Until await is called, f(2) is blocking (preventing any other task from being scheduled). Once we call await we explicitly inform the scheduler that we are waiting for something (usually I/O, but here simply "sleeping"). Similarly, f(1) blocks f(2) from going out until it calls await.

If we remove the blocking part (block for 0s) f(1) will be rescheduled to execute before f(2) and will therefore finish first:

>>> start = datetime.now()
>>> asyncio.create_task(f(2, block_for=0))
>>> asyncio.create_task(f(1, block_for=0))

2 IN     0s
2 AWAIT  0s
1 IN     0s
1 AWAIT  0s
1 OUT    1s
2 OUT    2s

Finally, about this part:

… how to schedule it to be called in the future?

You can have a look at how is asyncio.sleep() in python implemented?, it will probably help you understand better how asynchronous programming work.

cglacet
  • 8,873
  • 4
  • 45
  • 60