15. Event Loops

Doug Hellman in his mater book, “The Python 3 Standard Library by Example” (Developer’s Library) 1st Edition, Chapter 10.5, describes what an Event Loop is:

“Event loop, a first-class object that is responsible for efficiently handling I/O events, system events, and application context changes. Several loop implementations are provided, to take advantage of the operating systems’ capabilities efficiently. While a reasonable default is usually selected automatically, it is also possible to pick a particular event loop implementation from within the application”. [13]

Python documentation describes the event loop as follow: “Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses”. [14]

“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.”

MDN gives us the following definition of JavaScript event loop:

“JavaScript has a concurrency model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. “

In any case an event loop is as its name says , is a loop :-) . In Python we can implement loops with something so simple as a while statement . Also we can do it using a context Manager, etc.

In brief the Event Loop is an important part of any Async I/O architecture, and in Python this is also the case.

Specifically the asyncio module , one of many async i/o implementations in Python , creates the event loop in the BaseDefaultEventLoopPolicy class . This event loop runs in its own thread and what we expect from it ( efficiency, simplicity and responsibility in task delegation ) we certainly get it.

See below some lines of the source code for the method get_event_loop() in the BaseDefaultEventLoopPolicy class .

BaseDefaultEventLoop.

class BaseDefaultEventLoopPolicy(AbstractEventLoopPolicy):
    """Default policy implementation for accessing the event loop.
    In this policy, each thread has its own event loop.  However, we
    only automatically create an event loop by default for the main
    thread; other threads by default have no event loop.
    Other policies may have different rules (e.g. a single global
    event loop, or automatically creating an event loop per thread, or
    using some other notion of context to which an event loop is
    associated).
    """

    _loop_factory = None

    class _Local(threading.local):
        _loop = None
        _set_called = False

    def __init__(self):
        self._local = self._Local()

    def get_event_loop(self):
        """Get the event loop for the current context.
        Returns an instance of EventLoop or raises an exception.
        """
        if (self._local._loop is None and
                not self._local._set_called and
                isinstance(threading.current_thread(), threading._MainThread)):
            self.set_event_loop(self.new_event_loop())

        if self._local._loop is None:
            raise RuntimeError('There is no current event loop in thread %r.'
                               % threading.current_thread().name)

        return self._local._loop

David Beazley curio library uses an event loop object he called the Kernel. We ask the Kernel to run and pass as a parameter to it, a coroutine. One more time the Kernel ( Event Loop) has only limited coordination and functionality and is build on top of simple data structures, threads.Event object (real threads) etc

The curio Event Loop (Kernel) run function

def run(corofunc, *args, with_monitor=False, selector=None,
        debug=None, activations=None, **kernel_extra):
    '''
    Run the curio kernel with an initial task and execute until all
    tasks terminate.  Returns the task's final result (if any). This
    is a convenience function that should primarily be used for
    launching the top-level task of a curio-based application.  It
    creates an entirely new kernel, runs the given task to completion,
    and concludes by shutting down the kernel, releasing all resources used.

    Don't use this function if you're repeatedly launching a lot of
    new tasks to run in curio. Instead, create a Kernel instance and
    use its run() method instead.
    '''
    kernel = Kernel(selector=selector, debug=debug, activations=activations,
                    **kernel_extra)

    # Check if a monitor has been requested
    if with_monitor or 'CURIOMONITOR' in os.environ:
        from .monitor import Monitor
        m = Monitor(kernel)
        kernel._call_at_shutdown(m.close)
        kernel.run(m.start)

    with kernel:
        return kernel.run(corofunc, *args)

We have mentioned 3 Event loops two in Python and one in javascript the asyncio event loop is able to run asynchronous code ( threads , futures) as well as async i/o not blocking code

This is taken from the Python documentation

import asyncio
import concurrent.futures

def blocking_io():
    # File operations (such as logging) can block the
    # event loop: run them in a thread pool.
    with open('/dev/urandom', 'rb') as f:
        return f.read(100)

def cpu_bound():
    # CPU-bound operations will block the event loop:
    # in general it is preferable to run them in a
    # process pool.
    return sum(i * i for i in range(10 ** 7))

async def main():
    loop = asyncio.get_running_loop()

    ## Options:

    # 1. Run in the default loop's executor:
    result = await loop.run_in_executor(
        None, blocking_io)
    print('default thread pool', result)

    # 2. Run in a custom thread pool:
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, blocking_io)
        print('custom thread pool', result)

    # 3. Run in a custom process pool:
    with concurrent.futures.ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, cpu_bound)
        print('custom process pool', result)

asyncio.run(main())

As we can see, we used the asyncio Event Loop to run Process and Threads!!

Let’s see some more examples of Even Loops.

From Python asyncio docs :

import asyncio

def hello_world(loop):
    """A callback to print 'Hello World' and stop the event loop"""
    print('Hello World')
    loop.stop()

loop = asyncio.get_event_loop()

# Schedule a call to hello_world()
loop.call_soon(hello_world, loop)

# Blocking call interrupted by loop.stop()
try:
    loop.run_forever()
finally:
    loop.close()

More examples: