1

I've a situation like below,

event_loop = asyncio.new_event_loop()

async def second_async():
    # some async job
    print("I'm here")
    return 123

def sync():
    return asyncio.run_coroutine_threadsafe(second_async(), loop=event_loop).result()

async def first_async():
    sync()


event_loop.run_until_complete(first_async())

I call the sync function from a different thread(where the event_loop is not running), it works fine. The problem is if I run the event_loop.run_complete... line, the .result() call on the Task returned by run_coroutine_threadsafe blocks the execution of the loop, which makes sense. To avoid this, I tried changing this as follows,

event_loop = asyncio.new_event_loop()

async def second_async():
    # some async job
    print("I'm here")
    return 123


def sync():
    # if event_loop is running on current thread
        res = loop.create_task(second_async()).result()
    # else
        res = asyncio.run_coroutine_threadsafe(second_async(), loop=event_loop).result()
    
    # Additional processing on res
    # Need to evaluate the result of task right here in sync.
    return res

async def first_async():
    sync()


event_loop.run_until_complete(first_async())

This works fine, but the .result() call on the Task object returned by create_task always raises an InvalidStateError. The set_result is never called on the Task object.

Basically, I want the flow to be as such (async code) -> sync code (a non blocking call ->) async code

I know this is a bad way of doing things, but I'm integrating stuff, so I don't really have an option.

Crash0v3rrid3
  • 518
  • 2
  • 6

1 Answers1

0

Here is a little single-threaded program that illustrates the problem.

If you un-comment the line asyncio.run(first_async1()), you see the same error as you're seeing, and for the same reason. You're trying to access the result of a task without awaiting it first.

import asyncio
    
event_loop = asyncio.new_event_loop()

async def second_async():
    # some async job
    print("I'm here")
    return 123

def sync1():
    return asyncio.create_task(second_async()).result()

async def first_async1():
    print(sync1())

def sync2():
    return asyncio.create_task(second_async())

async def first_async2():
    print(await sync2())
    
# This prints I'm here,
# the raises invalid state error: 
# asyncio.run(first_async1())

# This works, prints "I'm here" and "123"
asyncio.run(first_async2())

With that line commented out again, the second version of the program (first_async2) runs just fine. The only difference is that the ordinary function, sync2, returns an awaitable instead of a result. The await is done in the async function that called it.

I don't see why this is a bad practice. To me, it seems like there are situations where it's absolutely necessary.

Another approach is to create a second daemon thread and set up an event loop there. Coroutines can be executed in this second thread with asyncio.run_coroutine_threadsafe, which returns a concurrent.futures.Future. Its result method will block until the Future's value is set by the other thread.

#! python3.8

import asyncio
import threading

def a_second_thread(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
loop2 = asyncio.new_event_loop()
threading.Thread(target=a_second_thread, args=(loop2,), daemon=True).start()

async def second_async():
    # some async job
    print("I'm here")
    for _ in range(4):
        await asyncio.sleep(0.25)
    print("I'm done")
    return 123

def sync1():
    # Run the coroutine in the second thread -> get a concurrent.futures.Future
    fut = asyncio.run_coroutine_threadsafe(second_async(), loop2)
    return fut.result()

async def first_async1():
    print(sync1())

def sync2():
    return asyncio.create_task(second_async())

async def first_async2():
    print(await sync2())
    
# This works, prints "I'm here", "I'm done", and "123"
asyncio.run(first_async1())

# This works, prints "I'm here", "I'm done", and "123"
asyncio.run(first_async2())

Of course this will still block the event loop in the main thread until fut.result() returns. There is no avoiding that. But the program runs.

Paul Cornelius
  • 9,245
  • 1
  • 15
  • 24
  • The app isn't this straightforward. I need to evaluate the task's result in the sync function. That's the problem. It's not just one sync function in between but multiple layers of sync code and I cannot modify all of it to return the task. – Crash0v3rrid3 Aug 14 '21 at 07:28
  • Usually, the intermediary sync code can be used to avoid additional coroutine creation and evaluation, but here the case is different. It'd make more sense to have all(the sync ones as well) functions as coroutine functions, but I'm restricted as the intermediary code is not mine and it's very gruesome to port to async. – Crash0v3rrid3 Aug 14 '21 at 07:32
  • Take a look at this: https://stackoverflow.com/questions/52783605/how-to-run-a-coroutine-outside-of-an-event-loop – Paul Cornelius Aug 15 '21 at 02:54
  • The guy explains the implementation of an event loop. His drive func isn't much useful cuz it uses time.sleep, which is a blocking call. It'd block the event loop. So, still nothing . – Crash0v3rrid3 Aug 15 '21 at 07:45
  • According to the doc, "asyncio Future is not compatible with the concurrent.futures.wait() and concurrent.futures.as_completed() functions.". This is exactly what I want supported. I'm looking for a workaround. – Crash0v3rrid3 Aug 15 '21 at 07:45
  • According to PEP 3156, https://www.python.org/dev/peps/pep-3156/, "In the future (pun intended) we may unify asyncio.Future and concurrent.futures.Future, e.g. by adding an __iter__() method to the latter that works with yield from. To prevent accidentally blocking the event loop by calling e.g. result() on a Future that's not done yet, the blocking operation may detect that an event loop is active in the current thread and raise an exception instead. However the current PEP strives to have no dependencies beyond Python 3.3, so changes to concurrent.futures.Future are off the table for now." – Crash0v3rrid3 Aug 15 '21 at 07:54
  • I'm not sure what you're looking at in the stackoverflow thread I linked. At the top of the accepted answer there is a minimum implementation of drive(), which does not call time.sleep. And even if it does call time.sleep(), the drive() function is forcing the coroutine OUTSIDE of the "real" event loop. If you block the event loop, no matter, because you are in effect substitute your own loop. There is no way, logically, for a synchronous function to yield control back to the event loop. Perhaps there is a good reason why unification of Futures is off the table. – Paul Cornelius Aug 15 '21 at 23:34
  • I modified my answer to add an alternative approach, using a special thread to execute coroutines. – Paul Cornelius Aug 16 '21 at 22:20
  • I'm already doing this. The problem is, the execution request can come from the same thread or from another one. If it comes from another one, that's great. I use run_coroutine_threadsafe and all works fine. The issue is when the execution request comes from the same thread. If I use run_coroutine_threadsafe, it blocks the whole event loop. Hence, I have to use create_task, but then I cannot yield back control to the event loop when I call .result() on the task. – Crash0v3rrid3 Aug 17 '21 at 04:34
  • If the request comes from the same thread, then call run_coroutine_threadsafe on the loop in the extra thread - what I'm calling loop2. That's the only thing that I'm using the extra thread for. If the request comes from a different thread, you already know what to do. In both cases, you are blocking the event loop in the calling thread until future.result() returns. – Paul Cornelius Aug 17 '21 at 05:05
  • That won't work if your coroutine accesses resources that aren't thread-safe. If your coroutine awaits on an asyncio.Event, for example, it will fail because the Event was initialized in a different thread. – Paul Cornelius Aug 17 '21 at 05:19
  • Ultimately, the resources are accessed and managed by the thread where the event loop is running. I'm delegating all I/O tasks to the event loop(from every thread). So I don't need to worry about it. – Crash0v3rrid3 Aug 17 '21 at 15:16
  • 1
    It seems to me like a perfectly good question. Obviously I thought it was interesting enough to try to answer :-) – Paul Cornelius Aug 18 '21 at 22:28