liveBook · Manning

Event loop

As explained in Grokking Concurrency

An event loop is a control-flow pattern that repeatedly waits for pending events, selects their handlers, and invokes those handlers.

Imagine ordering pizza by requesting a callback instead of waiting on the phone. You can do something else until the call comes. An event loop plays the coordinating role in a program: it waits for events, finds the callback registered for each one, runs it, and then looks for more work. section 11.2 section 11.3

A simple loop keeps arriving events in a queue and handles them one after another. An event handler can even register a new event. This differs from repeatedly checking whether something has happened: the loop waits for work rather than busy-waiting. Running handlers in turn is a form of concurrency, but it does not mean their code runs at the same time. section 11.3 section 11.5

From queue to pizza server

A small Python loop queues two knock events; each produces a who event. The loop handles both knocks, then both replies. A network pizza server uses the same coordinating idea with sockets: it registers callbacks for accepting connections, reading, and writing. Instead of repeatedly checking every client, it blocks in select until the operating system reports a socket ready for an operation, then invokes its callback. One thread can thereby serve multiple clients. section 11.3 section 11.5

# Chapter 11/event_loop.py
from collections import deque
from time import sleep
import typing as T

class Event:                                                       #1
    def __init__(self, name: str, action: T.Callable[..., None], 
                 next_event: T.Optional[Event] = None) -> None: 
        self.name = name 
        self._action = action
        self._next_event = next_event 

    def execute_action(self) -> None:
        self._action(self)
        if self._next_event:
            event_loop.register_event(self._next_event)

class EventLoop:
    def __init__(self) -> None:
        self._events: deque[Event] = deque()                       #2

    def register_event(self, event: Event) -> None:
        self._events.append(event)                                 #3

    def run_forever(self) -> None:
        print(f"Queue running with {len(self._events)} events")
        while True:                                                #4
            try:                                                   #4
                event = self._events.popleft()                     #4
            except IndexError:                                     #4
                continue                                           #4
            event.execute_action()                                 #4

def knock(event: Event) -> None:
    print(event.name)
    sleep(1)

def who(event: Event) -> None:
    print(event.name)
    sleep(1)

if __name__ == "__main__":
    event_loop = EventLoop()
    replying = Event("Who's there?", who)
    knocking = Event("Knock-knock", knock, replying)
    for _ in range(2):                                             #5
        event_loop.register_event(knocking)                        #5
    event_loop.run_forever()                                       #6

The Python listing shows the event queue, registration method, and loop behind the knock-and-reply example. section 11.3

The rule is simple but important: a handler must give control back. A blocking operation holds up the only event-loop thread, delaying every other event. Splitting an application among callbacks can also make its flow harder to read and debug, particularly when callbacks nest. section 12.1 section 11.2

Coroutines offer another way to share that thread. A coroutine pauses at yield, letting the loop run another ready task; the loop resumes it when its awaited work completes. A cooperative loop tracks ready tasks and sockets awaiting I/O, calling select when nothing is ready. It cannot interrupt a running coroutine: that task must yield. For CPU-heavy work, a thread pool can work alongside the loop and notify it when a result is ready. Neither approach makes blocking inside the loop harmless. section 12.4 section 12.8 section 12.10

How Grokking Concurrency covers it

How deep it goes
Works from a queued-event loop through select-based socket callbacks to ready-task scheduling, including blocking and cooperative-yield limits.
Examples
Uses queued knock-and-reply events, a select-based pizza server, and a coroutine-driven pizza server.
What it assumes
Introduces events and callbacks before assembling the loop, then builds toward sockets and coroutines.
What you can do after
Build it from scratch as a Python event queue or socket-ready dispatcher, and diagnose handlers that block the loop.
How it connects
Connects callbacks and event queues to I/O multiplexing, coroutines, futures, and cooperative multitasking.

Also explained in

Compare how 11 books explain event loop →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Concurrency. The section links point to the parts of the book it draws on.