9

If I use async functions, then all the functions above the stack should also be async, and their call should be preceded by the await keyword. This example emulates modern programs with several architectural layers of the application:

async def func1():
    await asyncio.sleep(1)

async def func2():
    await func1()

async def func3():
    await func2()

async def func4():
    await func3()

async def func5():
    await func4()

When an execution thread meet 'await', it can switch to another coroutine, which requires resources for context switching. With a large number of competing corutes and different levels of abstraction, these overheads may begin to limit the performance of the entire system. But in the presented example it makes sense to switch the context only in one case, on line:

await asyncio.sleep(1)

How can I ban context switching for certain asynchronous functions?

Benyamin Jafari
  • 27,880
  • 26
  • 135
  • 150
SemenDr
  • 111
  • 1
  • 3
  • 1
    Note that ``await`` by itself does not cause switching of coroutines. See [Does await always give other tasks a chance to execute?](https://stackoverflow.com/questions/59996493/does-await-always-give-other-tasks-a-chance-to-execute) – MisterMiyagi Sep 23 '20 at 10:36

2 Answers2

17

First of all, by default in your example context wouldn't be switched. In other words, until coroutine faces something actually blocking (like Future) it won't return control to event loop and resume its way directly to an inner coroutine.

I don't know easier way to demonstrate this than inheriting default event loop implementation:

import asyncio


class TestEventLoop(asyncio.SelectorEventLoop):
    def _run_once(self):
        print('control inside event loop')
        super()._run_once()


async def func1():
    await asyncio.sleep(1)


async def func2():
    print('before func1')
    await func1()
    print('after func1')


async def main():
    print('before func2')
    await func2()
    print('after func2')


loop = TestEventLoop()
asyncio.set_event_loop(loop)
try:
    loop.run_until_complete(main())
finally:
    loop.close()

In output you'll see:

control inside event loop
before func2
before func1
control inside event loop
control inside event loop
after func1
after func2
control inside event loop

func2 passed execution flow directly to func1 avoiding event loop's _run_once that could switch to another coroutine. Only when blocking asyncio.sleep was faced, event loop got control.

Although it's a detail of implementation of default event loop.


Second of all, and it's probably much more important, switching between coroutines is extremely cheap comparing to benefit we get from using asyncio to work with I/O.

It's also much cheaper than other async alternatives like switching between OS threads.

Situation when your code is slow because of many coroutines is highly unlikely, but even if it happened you should probably to take a look at more efficient event loop implementations like uvloop.

Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • @SemenDr you're welcome! If you think answer is a solution to your problem feel free to accept it as explained here: https://stackoverflow.com/help/someone-answers – Mikhail Gerasimov Feb 26 '19 at 05:48
  • Is this behavior part of the standard? Or is it more of an implementation detail that we can't rely on? – frosthamster Aug 15 '23 at 11:04
  • @frosthamster you can't rely that every `await` will pass the control to event loop. To unsure the control is passed, you can write `await asyncio.sleep(0)`. But it's up to the event loop to decide which coroutine will be awakened next. – Mikhail Gerasimov Aug 18 '23 at 11:29
2

I would like to point out that if you ever run a sufficiently large number of coroutines that the overhead of switching context becomes an issue, you can ensure reduced concurrency using a Semaphore. I recently received a ~2x performance increase by reducing concurrency from 1000 to 50 for coroutines running HTTP requests.

Ryan Codrai
  • 172
  • 8