This post is about python asyncio. asyncio is python asynchronous implementation providing event loop functionality. From “event loop” wiki:

In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external “event provider” (that generally blocks the request until an event has arrived), then calls the relevant event handler (“dispatches the event”). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.

asyncio defines awaitables and one important awaitables is coroutine which is a python task with keyword async. coroutines are called with await from other routines.

In [1]: import asyncio

In [2]: async def main():
   ...: ...     print('hello')
   ...: ...     await asyncio.sleep(1)
   ...: ...     print('world')
   ...:

In [3]: main
Out[3]: <function __main__.main()>

In [4]: main()
Out[4]: <coroutine object main at 0x7f988a718040>

In [5]: asyncio.run(main())
hello
world

asyncio.run starts the event loop and adds the coroutines. From the help:

run(main, *, debug=None)
    Execute the coroutine and return the result.

    This function runs the passed coroutine, taking care of
    managing the asyncio event loop and finalizing asynchronous
    generators.

    This function cannot be called when another asyncio event loop is
    running in the same thread.

At this point, I am not done yet. I thought to dip my toes into cpython. So, Jumping into cpython, run is imported from cpython/Lib/asyncio/runners.py

188     with Runner(debug=debug) as runner:
189         return runner.run(main)

which calls run from Runner in the same file. The _loop is init with self._loop = events.new_event_loop() inside _lazy_init. Then task is created a and passed to run_until_complete.

 85     def run(self, coro, *, context=None):

    ...
    ...

 99         task = self._loop.create_task(coro, context=context)
100
101         if (threading.current_thread() is threading.main_thread()
102             and signal.getsignal(signal.SIGINT) is signal.default_int_handler
103         ):
104             sigint_handler = functools.partial(self._on_sigint, main_task=task)
105             try:
106                 signal.signal(signal.SIGINT, sigint_handler)
107             except ValueError:
108                 # `signal.signal` may throw if `threading.main_thread` does
109                 # not support signals (e.g. embedded interpreter with signals
110                 # not registered - see gh-91880)
111                 sigint_handler = None
112         else:
113             sigint_handler = None
114
115         self._interrupt_count = 0
116         try:
117             return self._loop.run_until_complete(task)

There is section above where it uses partial to pass task to self._on_sigint when SIGINT happens.

create_task is called to return task. Task is defined in tasks.py

 429     def create_task(self, coro, *, name=None, context=None):
 430         """Schedule a coroutine object.
 431
 432         Return a task object.
 433         """
 434         self._check_closed()
 435         if self._task_factory is None:
 436             task = tasks.Task(coro, loop=self, name=name, context=context)
 437             if task._source_traceback:
 438                 del task._source_traceback[-1]
 439         else:
 440             if context is None:
 441                 # Use legacy API if context is not needed
 442                 task = self._task_factory(self, coro)
 443             else:
 444                 task = self._task_factory(self, coro, context=context)
 445
 446             tasks._set_task_name(task, name)
 447
 448         return task

my laptop battery is dying. So, I will have to do part 2 of whatever this is.