Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent context switching when calling an async function?

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?

like image 784
SemenDr Avatar asked Feb 25 '19 05:02

SemenDr


2 Answers

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.

like image 87
Mikhail Gerasimov Avatar answered Oct 08 '22 00:10

Mikhail Gerasimov


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.

like image 43
Ryan Codrai Avatar answered Oct 07 '22 23:10

Ryan Codrai