Make a Python asyncio call from a Flask route

前端 未结 5 608
迷失自我
迷失自我 2020-12-07 15:31

I want to execute an async function every time the Flask route is executed. Why is the abar function never executed?

import asyncio
from flask          


        
5条回答
  •  北荒
    北荒 (楼主)
    2020-12-07 16:35

    Your mistake is to try to run the asyncio event loop after calling app.run(). The latter doesn't return, it instead runs the Flask development server.

    In fact, that's how most WSGI setups will work; either the main thread is going to busy dispatching requests, or the Flask server is imported as a module in a WSGI server, and you can't start an event loop here either.

    You'll instead have to run your asyncio event loop in a separate thread, then run your coroutines in that separate thread via asyncio.run_coroutine_threadsafe(). See the Coroutines and Multithreading section in the documentation for what this entails.

    Here is an implementation of a module that will run such an event loop thread, and gives you the utilities to schedule coroutines to be run in that loop:

    import asyncio
    import itertools
    import time
    import threading
    
    __all__ = ["EventLoopThread", "get_event_loop", "stop_event_loop", "run_coroutine"]
    
    class EventLoopThread(threading.Thread):
        loop = None
        _count = itertools.count(0)
    
        def __init__(self):
            name = f"{type(self).__name__}-{next(self._count)}"
            super().__init__(name=name, daemon=True)
    
        def __repr__(self):
            loop, r, c, d = self.loop, False, True, False
            if loop is not None:
                r, c, d = loop.is_running(), loop.is_closed(), loop.get_debug()
            return (
                f"<{type(self).__name__} {self.name} id={self.ident} "
                f"running={r} closed={c} debug={d}>"
            )
    
        def run(self):
            self.loop = loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
    
            try:
                loop.run_forever()
            finally:
                try:
                    shutdown_asyncgens = loop.shutdown_asyncgens()
                except AttributeError:
                    pass
                else:
                    loop.run_until_complete(shutdown_asyncgens)
                loop.close()
                asyncio.set_event_loop(None)
    
        def stop(self):
            loop, self.loop = self.loop, None
            if loop is None:
                return
            loop.call_soon_threadsafe(loop.stop)
            self.join()
    
    _lock = threading.Lock()
    _loop_thread = None
    
    def get_event_loop():
        global _loop_thread
        with _lock:
            if _loop_thread is None:
                _loop_thread = EventLoopThread()
                _loop_thread.start()
            return _loop_thread.loop
    
    def stop_event_loop():
        global _loop_thread
        with _lock:
            if _loop_thread is not None:
                _loop_thread.stop()
                _loop_thread = None
    
    def run_coroutine(coro):
        """Run the coroutine in the event loop running in a separate thread
    
        Returns a Future, call Future.result() to get the output
    
        """
        return asyncio.run_coroutine_threadsafe(coro, get_event_loop())
    

    You can use the run_coroutine() function defined here to schedule asyncio routines. Use the returned Future instance to control the coroutine:

    • Get the result with Future.result(). You can give this a timeout; if no result is produced within the timeout, the coroutine is automatically cancelled.
    • You can query the state of the coroutine with the .cancelled(), .running() and .done() methods.
    • You can add callbacks to the future, which will be called when the coroutine has completed, or is cancelled or raised an exception (take into account that this is probably going to be called from the event loop thread, not the thread that you called run_coroutine() in).

    For your specific example, where abar() doesn't return any result, you can just ignore the returned future, like this:

    @app.route("/")
    def notify():
        run_coroutine(abar("abar"))
        return "OK"
    

    Note that before Python 3.8 that you can't use an event loop running on a separate thread to create subprocesses with! See my answer to Python3 Flask asyncio subprocess in route hangs for backport of the Python 3.8 ThreadedChildWatcher class for a work-around for this.

提交回复
热议问题