Michael
Michael

Reputation: 2377

Starvation in `asyncio` loop

I have a system where two "processes" A and B run on the same asyncio event loop.

I notice that the order of the initiation of processes matters - i.e. if I start process B first then process B runs all the time, while it seems that A is being "starved" of resources vise-a-versa.

In my experience, the only reason this might happen is due to a mutex which is not being released by B, but in the following toy example it happens without any mutexs being used:

import asyncio


async def A():
    while True:
        print('A')
        await asyncio.sleep(2)


async def B():
    while True:
        print('B')
        await asyncio.sleep(8)


async def main():
    await B()
    await A()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Is in python the processes do not perform context-switch automatically? If not - how can I make both processes participate, each one in the time the other one is idle (i.e., sleeping)?

Upvotes: 3

Views: 942

Answers (1)

MisterMiyagi
MisterMiyagi

Reputation: 51999

TLDR: Coroutines merely enable concurrency, they do not automatically trigger concurrency. Explicitly launch separate tasks, e.g. via create_task or gather, to run the coroutines concurrently.

async def main():
    await asyncio.gather(B(), A())

Concurrency in asyncio is handled via Tasks – a close equivalent to Threads – which merely consist of coroutines/awaitables – like Threads consist of functions/callables. In general, a coroutine/awaitable itself does not equate to a separate task.

Using await X() means "start X and wait for it to complete". When using several such constructs in sequence:

async def main():
    await B()
    await A()

this means launching B first, and only launching A after B has completed: while async def and await allows for concurrency towards other tasks, B and A are run sequentially with respect to each other in a single task.

The simplest means to add concurrency is to explicitly create a task:

async def main():
    # execute B in a new task
    b_task = asyncio.create_task(B())
    # execute A in the current task
    await A()
    await b_task

Note how B is offloaded to a new task, while one can still do a final await A() to re-use the current task.

Most async frameworks ship with high-level helpers for common concurrency scenarios. In this case, asyncio.gather is appropriate to launch several tasks at once:

async def main():
    # execute B and A in new tasks
    await asyncio.gather(B(), A())

Upvotes: 5

Related Questions