Asynchronous programming is a means of programming in which a unit of work runs separately from the main application thread and notifies the calling thread of its completion, failure or progress.
It runs tasks one after the other. At any given time, only one of the tasks is running.
As you can imagine, there is a lot of pressure on the active task, since other tasks are waiting for their turn. So, when the active task makes a blocking call, say a network request, and cannot make further progress it gives the control back to the event loop realizing that some other task could possibly better utilize the event loop’s time. It also tells the event loop what exactly it is blocked upon, so that when the network response comes, the event loop can consider giving it time to run again.
Coroutines (co-operative routines) are a key element of the symphony. It is the coroutines, and their co-operative nature, that enables giving up control of the event loop, when the coroutine has nothing useful to do. A coroutine is a stateful generalisation of the concept of subroutine.
when a coroutine “returns” (yields control) it simply means that it has paused its execution (with some saved state). So when you “invoke” (give control to) the coroutine subsequently, it would be correct to say that the coroutine has resumed its execution
import asyncio
async def foo():
print('Running in foo')
await asyncio.sleep(0)
print('Explicit context switch to foo again')
async def bar():
print('Explicit context to bar')
await asyncio.sleep(0)
print('Implicit context switch back to bar')
async def main():
tasks = [foo(), bar()]
await asyncio.gather(*tasks)
asyncio.run(main())