54

I am using Python3 Asyncio module to create a load balancing application. I have two heavy IO tasks:

  • A SNMP polling module, which determines the best possible server
  • A "proxy-like" module, which balances the petitions to the selected server.

Both processes are going to run forever, are independent from eachother and should not be blocked by the other one.

I cant use 1 event loop because they would block eachother, is there any way to have 2 event loops or do I have to use multithreading/processing?

I tried using asyncio.new_event_loop() but havent managed to make it work.

brunoop
  • 949
  • 1
  • 7
  • 10
  • 6
    If designed the right way, Asyncio coroutines won't block each other even though they run on the same loop. Asyncio effectively switches back and forth between multiple coroutines / tasks to give the effect of concurrency, even if using a single thread. – songololo Jul 25 '15 at 08:43
  • 1
    @shongololo but if I have one loop running with "loop.run_forever()" it blocks the loop and I cant do anything else unless i stop it. Or am I undertanding wrong? Thats the behaviour I am seeing... – brunoop Jul 25 '15 at 23:36
  • 1
    not sure I fully understand the dilemma. Is there anything stopping you from running both in the same loop? asyncio will automatically switch back and forth (within the same loop) when encountering 'yield from' points inside your code. This is basically the point of asyncio, it lets you run multiple and potentially blocking coroutines inside the same loop without one blocking the other. – songololo Jul 26 '15 at 14:56
  • If the proxy server is running all the time it cannot switch back and forth. The proxy listens for client requests and makes them asynchronous, but the other task cannot execute, because this one is serving forever. – brunoop Jul 27 '15 at 01:09
  • 5
    The whole point of asyncio is that you can run multiple thousands of I/O-heavy tasks concurrently, so you don't need Threads at all, this is exactly what asyncio is made for. Just run the two coroutines (SNMP and proxy) in the same loop and that's it. On the technical side: You have to make both of them available to the event loop BEFORE calling `loop.run_forever()` – kissgyorgy Oct 27 '18 at 16:35
  • 1
    @kissgyorgy that's just the ideal case. In reality, even the file operations are not fully supported async-operations, and those are usually wrapped in threads. brunoop, please read my answer below. https://stackoverflow.com/a/62631135/1592410 – Johann Chang Jun 29 '20 at 04:31

6 Answers6

58

The whole point of asyncio is that you can run multiple thousands of I/O-heavy tasks concurrently, so you don't need Threads at all, this is exactly what asyncio is made for. Just run the two coroutines (SNMP and proxy) in the same loop and that's it. You have to make both of them available to the event loop BEFORE calling loop.run_forever(). Something like this:

import asyncio

async def snmp():
    print("Doing the snmp thing")
    await asyncio.sleep(1)

async def proxy():
    print("Doing the proxy thing")
    await asyncio.sleep(2)

async def main():
    while True:
        await snmp()
        await proxy()

loop = asyncio.get_event_loop()
loop.create_task(main())
loop.run_forever()

I don't know the structure of your code, so the different modules might have their own infinite loop or something, in this case you can run something like this:

import asyncio

async def snmp():
    while True:
        print("Doing the snmp thing")
        await asyncio.sleep(1)

async def proxy():
    while True:
        print("Doing the proxy thing")
        await asyncio.sleep(2)

loop = asyncio.get_event_loop()
loop.create_task(snmp())
loop.create_task(proxy())
loop.run_forever()

Remember, both snmp and proxy needs to be coroutines (async def) written in an asyncio-aware manner. asyncio will not make simple blocking Python functions suddenly "async".

In your specific case, I suspect that you are confused a little bit (no offense!), because well-written async modules will never block each other in the same loop. If this is the case, you don't need asyncio at all and just simply run one of them in a separate Thread without dealing with any asyncio stuff.

kissgyorgy
  • 2,947
  • 2
  • 32
  • 54
  • 1
    what if one does not know all the tasks will be needed to be run? – Jako Aug 30 '19 at 08:17
  • 1
    @Jako It's like asking if "how do I call a function which I don't know the name of?" You can't. – kissgyorgy Oct 10 '19 at 23:25
  • "... you don't need Threads at all ..." What about `ThreadPoolExecutor`? – Johann Chang Jun 29 '20 at 04:25
  • @changyuheng Since you don't need to use multiple threads, you don't need a thread pool either. – augurar Sep 29 '20 at 07:27
  • 1
    @kissgyorgy Technically, you _can_ call functions you don't have the name of -- especially if they have no name. Simplest example: `(lambda x: x)(42)`. ;) – Mateen Ulhaq Nov 19 '20 at 06:03
  • 2
    @MateenUlhaq %s/know the name of/have a reference for/" – kissgyorgy Jan 12 '21 at 17:17
  • This is the general theory. In practice there's cases where you have CPU intensive tasks that need to be mixed iwth slow I/O work. Then it makes sense to have 1 or more event/asyncio loops and separate workers. – Pithikos Jun 12 '23 at 14:01
  • @Pithikos Yes, that's True, however in that case, you must use multiprocessing, because with threads, the program would be slower (because of the GIL). Use the [ProcessPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor) in that case! – kissgyorgy Jun 13 '23 at 19:17
33

Answering my own question to post my solution:

What I ended up doing was creating a thread and a new event loop inside the thread for the polling module, so now every module runs in a different loop. It is not a perfect solution, but it is the only one that made sense to me(I wanted to avoid threads, but since it is only one...). Example:

import asyncio
import threading


def worker():
    second_loop = asyncio.new_event_loop()
    execute_polling_coroutines_forever(second_loop)
    return

threads = []
t = threading.Thread(target=worker)
threads.append(t)
t.start()

loop = asyncio.get_event_loop()
execute_proxy_coroutines_forever(loop)

Asyncio requires that every loop runs its coroutines in the same thread. Using this method you have one event loop foreach thread, and they are totally independent: every loop will execute its coroutines on its own thread, so that is not a problem. As I said, its probably not the best solution, but it worked for me.

brunoop
  • 949
  • 1
  • 7
  • 10
  • 2
    Using `asyncio.set_event_loop()` in the worker threads can let you omit passing loop instance manually. Please see my answer below: https://stackoverflow.com/a/62631135/1592410 – Johann Chang Jul 06 '20 at 08:41
20

Though in most cases, you don't need multiple event loops running when using asyncio, people shouldn't assume their assumptions apply to all the cases or just give you what they think are better without directly targeting your original question.

Here's a demo of what you can do for creating new event loops in threads. Comparing to your own answer, the set_event_loop does the trick for you not to pass the loop object every time you do an asyncio-based operation.

import asyncio
import threading


async def print_env_info_async():
    # As you can see each work thread has its own asyncio event loop.
    print(f"Thread: {threading.get_ident()}, event loop: {id(asyncio.get_running_loop())}")


async def work():
    while True:
        await print_env_info_async()
        await asyncio.sleep(1)


def worker():
    new_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(new_loop)
    new_loop.run_until_complete(work())
    return


number_of_threads = 2
for _ in range(number_of_threads):
    threading.Thread(target=worker).start()

Ideally, you'll want to put heavy works in worker threads and leave the asncyio thread run as light as possible. Think the asyncio thread as the GUI thread of a desktop or mobile app, you don't want to block it. Worker threads are usually very busy, this is one of the reason you don't want to create separate asyncio event loops in worker threads. Here's an example of how to manage heavy worker threads with a single asyncio event loop. And this is the most common practice in this kind of use cases:

import asyncio
import concurrent.futures
import threading
import time


def print_env_info(source_thread_id):
    # This will be called in the main thread where the default asyncio event loop lives.
    print(f"Thread: {threading.get_ident()}, event loop: {id(asyncio.get_running_loop())}, source thread: {source_thread_id}")


def work(event_loop):
    while True:
        # The following line will fail because there's no asyncio event loop running in this worker thread.
        # print(f"Thread: {threading.get_ident()}, event loop: {id(asyncio.get_running_loop())}")
        event_loop.call_soon_threadsafe(print_env_info, threading.get_ident())
        time.sleep(1)


async def worker():
    print(f"Thread: {threading.get_ident()}, event loop: {id(asyncio.get_running_loop())}")
    loop = asyncio.get_running_loop()
    number_of_threads = 2
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=number_of_threads)
    for _ in range(number_of_threads):
        asyncio.ensure_future(loop.run_in_executor(executor, work, loop))


loop = asyncio.get_event_loop()
loop.create_task(worker())
loop.run_forever()
Johann Chang
  • 1,281
  • 2
  • 14
  • 25
  • 2
    you may want to update ensure_future to create_task as the [asyncio documentation](https://devdocs.io/python~3.8/library/asyncio-task#creating-tasks) indicates that create_task is less readable. Great answer by the way! This was super helpful! – bryon Aug 14 '20 at 19:16
7

I know it's an old thread but it might be still helpful for someone. I'm not good in asyncio but here is a bit improved solution of @kissgyorgy answer. Instead of awaiting each closure separately we create list of tasks and fire them later (python 3.9):

import asyncio

async def snmp():
    while True:
        print("Doing the snmp thing")
        await asyncio.sleep(0.4)

async def proxy():
    while True:
        print("Doing the proxy thing")
        await asyncio.sleep(2)

async def main():
        tasks = []
        tasks.append(asyncio.create_task(snmp()))
        tasks.append(asyncio.create_task(proxy()))

        await asyncio.gather(*tasks)

asyncio.run(main())

Result:

Doing the snmp thing
Doing the proxy thing
Doing the snmp thing
Doing the snmp thing
Doing the snmp thing
Doing the snmp thing
Doing the proxy thing
tamerlaha
  • 1,902
  • 1
  • 17
  • 25
0

Asyncio event loop is a single thread running and it will not run anything in parallel, it is how it is designed. The closest thing which I can think of is using asyncio.wait.

from asyncio import coroutine
import asyncio

@coroutine
def some_work(x, y):
    print("Going to do some heavy work")
    yield from asyncio.sleep(1.0)
    print(x + y)

@coroutine
def some_other_work(x, y):
    print("Going to do some other heavy work")
    yield from asyncio.sleep(3.0)
    print(x * y)



if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait([asyncio.async(some_work(3, 4)), 
                            asyncio.async(some_other_work(3, 4))]))
    loop.close()

an alternate way is to use asyncio.gather() - it returns a future results from the given list of futures.

tasks = [asyncio.Task(some_work(3, 4)), asyncio.Task(some_other_work(3, 4))]
loop.run_until_complete(asyncio.gather(*tasks))
Nihal Sharma
  • 2,397
  • 11
  • 41
  • 57
  • I know that event loops are single-threaded, thats why I was asking if there would be any way to implement it using 2 different loops. I cannot use that method because as I said, every coroutine will run forever, they will not end. Will try to post my solution when I figure it out, Thanks. – brunoop Jul 26 '15 at 04:32
  • 3
    @brunoop You can still use this approach - it doesn't matter that your tasks don't end. Just schedule the coroutines you need using `asyncio.async(your_coroutine())`, and then call `loop.run_forever()` once they've all been scheduled. There's no need to use two threads. – dano Jul 27 '15 at 03:41
0

If the proxy server is running all the time it cannot switch back and forth. The proxy listens for client requests and makes them asynchronous, but the other task cannot execute, because this one is serving forever.

If the proxy is a coroutine and is starving the SNMP-poller (never awaits), isn't the client requests being starved aswell?

every coroutine will run forever, they will not end

This should be fine, as long as they do await/yield from. The echo server will also run forever, it doesn't mean you can't run several servers (on differents ports though) in the same loop.