Coder Social home page Coder Social logo

Comments (8)

douglas-raillard-arm avatar douglas-raillard-arm commented on May 24, 2024

Actually it is known for sure the asyncio's event loop is not thread safe.

However it would make sense for the patched asyncio.run() to actually be thread safe by using asyncio.run_coroutine_threadsafe() if possible, to make it look like asyncio.run() is thread safe.

If that is not the case nest_asyncio become quite more difficult to use as a way to add an async API to an existing library without breaking compatibility such as devlib

  • existing code exposed a thread safe API
  • The code is rewritten in async style to enable easy concurrency with a good API.
  • Blocking stubs are still provided for all converted function. This is done automatically using a decorator in this way:
@asyncf
async def foo(..):
   ...


# Call the coroutine function directly
foo.asyn() 

# blocking call, for backward compat and simple uses.
# It's equivalent to asyncio.run(foo.asyn())
foo() 

That means that arbitrary code can now call asyncio.run() in order to provide a backward-compatible blocking stub. These calls might very well happen from user code from multiple threads.

from nest_asyncio.

erdewit avatar erdewit commented on May 24, 2024

asyncio.run is not thread-safe and the patched version isn't either. Your example code is therefore also not thread-safe.

The actual error is from closing the "self pipe" in the garbage collection of the event loop. I remember that this was a randomly occuring bug in Python, dependent on the order of how things are cleaned up.

As an alternative, it's possible to give the threads of the threadpool each their own event loop (via the initalizer of the pool) and then run the tasks in the thread's own loop with loop.run_until_complete. Perhaps that covers your use case.

from nest_asyncio.

douglas-raillard-arm avatar douglas-raillard-arm commented on May 24, 2024

I'll see what I can do with a per-thread event loop thanks. The story around transitioning to async without breaking backward compat is really awful in Python. It feels like it was not even an afterthought but not thought at all, even years after the introduction of async ...

from nest_asyncio.

douglas-raillard-arm avatar douglas-raillard-arm commented on May 24, 2024

@erdewit Actually I'm a bit confused. The doc states:

This function always creates a new event loop and closes it at the end. It should be used as a main entry point for asyncio programs, and should ideally only be called once.
https://docs.python.org/3/library/asyncio-task.html#asyncio.run

While it does not specify it's thread safe, it clearly states that a new event loop is created, so I'm not sure rolling my own equivalent of asyncio.run() would fix anything.
Users typically create their own threads, how could I create a new loop in that case ?
Alternatively I could probably just setup a loop for the main thread in a global var and then call asyncio.run_coroutine_threadsafe() from the other threads, but that would not scale as well.

I remember that this was a randomly occuring bug in Python, dependent on the order of how things are cleaned up.
Is it something that only shows up in multithreaded code ? Python 3.9 seems to not exhibit the problem though so I can live with that.

from nest_asyncio.

erdewit avatar erdewit commented on May 24, 2024

While it does not specify it's thread safe, it clearly states that a new event loop is created

You're right, asyncio.run should be thread-safe if the tasks are thread-safe too. For the nested version it is not possible to create a new loop for every run, since there can be multiple runs concurrently in the same thread and they all have to use the same event loop.

Alternatively I could probably just setup a loop for the main thread in a global var and then call asyncio.run_coroutine_threadsafe()

It would defeat the purpose of using a thread pool, which supposedly is to run CPU intensive code that can release the GIL. If it is run in the main thread it will block the main event loop.

from nest_asyncio.

douglas-raillard-arm avatar douglas-raillard-arm commented on May 24, 2024

As long as asyncio.run() is thread-safe I think I'm good since the lib itself is already threadsafe.

It would defeat the purpose of using a thread pool, which supposedly is to run CPU intensive code that can release the GIL. If it is run in the main thread it will block the main event loop.

Yes in absolute terms, not so much in practice in my case. The library in question abstracts over adb/ssh/localhost shell interactions. The reason it allowed multithreading was to allow use cases similar to what async nowadays provides.

The serial part of the code is pretty cheap compared to the command executions themselves (which can take >50ms). The management of the command execution is done in a separate thread pool, so the event loop itself would not have much to do beyond dispatching the commands. The code using the output of the said commands is very cheap, so even if a lot of multithread users end up scheduling coroutines on the same loop it should not be too horrible.

That said, I just hit an issue while prototyping that: asyncio.run_coroutine_threadsafe() returns a concurrent.futures.Future. If the loop.run_forever() happens in another thread, future.result() hangs and so does the event loop for some reason. Simply waiting with time.sleep() shows that without the .result() call, the event loop proceeds to execute everything as expected. I have not yet found anyone else complaining about that (and it's not related to nest_asyncio):

import atexit
import threading
import asyncio

# import nest_asyncio
# nest_asyncio.apply()

_LOOP = asyncio.new_event_loop()
def _loop_threadf():
    loop = _LOOP
    try:
        loop.run_forever()
    finally:
        loop.close()

def _loop_stop():
    _LOOP.call_soon_threadsafe(_LOOP.stop)
    # Since the thread is a daemon thread, we need to join() it here otherwise
    # the main thread will keep going and then abruptly kill the daemon thread
    # at some point.
    try:
        _LOOP_THREAD.join()
    # If the thread did not start already, we will get a RuntimeError
    except RuntimeError:
        pass


# We need to use a daemon thread, otherwise the atexit handler will not run for
# some reason.
_LOOP_THREAD = threading.Thread(
    target=_loop_threadf,
    name='devlib event loop',
    daemon=True
)
atexit.register(_loop_stop)
_LOOP_THREAD.start()



def run(coro):
    future = asyncio.run_coroutine_threadsafe(coro, _LOOP)
    # Uncomment that and corof12 is never printed, which indicates the event
    # loop froze
    #
    # return future.result()

async def corof2():
    print('corof2')

async def corof1():
    print('corof1')
    run(corof2())
    # await corof2()

run(corof1())

import time
time.sleep(2)

from nest_asyncio.

erdewit avatar erdewit commented on May 24, 2024
def run(coro):
    future = asyncio.run_coroutine_threadsafe(coro, _LOOP)
    return future.result()

This will only work if _LOOP is running in a different thread. It will deadlock if in the same thread, as it will get stuck at the last line and and the task can't make any progress since the event loop is blocked.

from nest_asyncio.

douglas-raillard-arm avatar douglas-raillard-arm commented on May 24, 2024

It will deadlock if in the same thread, as it will get stuck at the last line and and the task can't make any progress since the event loop is blocked.

The loop is supposed to make progress with the run_forever() call, done in a separate thread. Also it does "work" as long you never call future.result(), which is quite strange (you can see the prints proving that the coroutine is actually being executed).

EDIT: I've been staring at that stuff for too long, it's actually quite obvious that it deadlocks indeed. The nested run() obviously does not yield back to the event loop since there is no await, and the whole thing deadlocks.

from nest_asyncio.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.