pythonpython-3.xmultithreadingpython-asyncio

How do I set the asyncio event loop for a thread in Python?


I'm trying to create two threads that each have their own asyncio event loop.

I've tried the following code but it doesn't seem to work:

import asyncio
from threading import Thread

def hello(thread_name):
    print('hello from thread {}!'.format(thread_name))

event_loop_a = asyncio.new_event_loop()
event_loop_b = asyncio.new_event_loop()

def callback_a():
    asyncio.set_event_loop(event_loop_a)
    asyncio.get_event_loop().call_soon_threadsafe(lambda: hello('a'))

def callback_b():
    asyncio.set_event_loop(event_loop_b)
    asyncio.get_event_loop().call_soon_threadsafe(lambda: hello('b'))

thread_a = Thread(target=callback_a, daemon=True)
thread_b = Thread(target=callback_b, daemon=True)
thread_a.start()
thread_b.start()

My use case is calling Tornado web framework's websocket_connect async function.


Solution

  • Your threads are queuing callbacks in their respective event loops, but they exit before actually running the event loop, so the callbacks never get executed. Also, you don't need call_soon_threadsafe, since you are invoking the callback from the same thread the event loop is (or, rather, will be) running on.

    This code prints the expected output:

    import asyncio
    from threading import Thread
    
    def hello(thread_name):
        print('hello from thread {}!'.format(thread_name))
    
    event_loop_a = asyncio.new_event_loop()
    event_loop_b = asyncio.new_event_loop()
    
    def callback_a():
        asyncio.set_event_loop(event_loop_a)
        asyncio.get_event_loop().call_soon(lambda: hello('a'))
        event_loop_a.run_forever()
    
    def callback_b():
        asyncio.set_event_loop(event_loop_b)
        asyncio.get_event_loop().call_soon(lambda: hello('b'))
        event_loop_b.run_forever()
    
    thread_a = Thread(target=callback_a, daemon=True)
    thread_b = Thread(target=callback_b, daemon=True)
    thread_a.start()
    thread_b.start()
    

    A more typical use case for call_soon_threadsafe, and more in line with what you might have had in mind, is to submit a callback (or a coroutine using asyncio.run_coroutine_threadsafe) to an event loop running in another thread. Here is an example:

    import asyncio, threading
    
    def hello(thread_name):
        print('hello from thread {}!'.format(thread_name))
    
    event_loop_a = asyncio.new_event_loop()
    event_loop_b = asyncio.new_event_loop()
    
    def run_loop(loop):
        asyncio.set_event_loop(loop)
        loop.run_forever()
    
    threading.Thread(target=lambda: run_loop(event_loop_a)).start()
    threading.Thread(target=lambda: run_loop(event_loop_b)).start()
    
    event_loop_a.call_soon_threadsafe(lambda: hello('a'))
    event_loop_b.call_soon_threadsafe(lambda: hello('b'))
    
    event_loop_a.call_soon_threadsafe(event_loop_a.stop)
    event_loop_b.call_soon_threadsafe(event_loop_b.stop)
    

    In that case it is rarely needed to have more than one event loop thread - you'd typically create only one, and allow that thread to service all your asyncio needs. After all, being able to host a large number of tasks in a single event loop is one of the strong points of asyncio.