C++ / a working model

137 / 163   ·   C11   ·   8 min

Event-based Concurrency (Advanced)

Keep this sentence

This chapter presents a way to build concurrent servers without threads. The program centers on an event loop that processes one arriving event at a time, giving the developer full scheduling control and removing any need for locks. The essential restriction is that handlers must never perform operations that can block.

In this lesson
  1. How the Event Loop Operates
  2. Watching Multiple Descriptors with select
  3. Single-Threaded Execution Removes Locks
  4. A Blocking Call Freezes the Whole Loop
  5. Example
  6. Exercise

Official chapter PDF

How the Event Loop Operates

The server enters a never-ending loop. Each iteration first gathers the currently pending events and then invokes the matching handler for each of them in turn. A handler performs only a small amount of work before returning, so the loop always decides what runs next. The developer therefore chooses the order of tasks directly instead of leaving that choice to the operating-system scheduler.

Watching Multiple Descriptors with select

select lets a program ask, in one call, whether any of a set of file descriptors is ready for reading, writing or has an error. The caller supplies the interesting descriptors and an optional timeout. On return the sets contain only the descriptors that are truly ready, so the loop processes just those descriptors and neither spins nor blocks unexpectedly.

Single-Threaded Execution Removes Locks

The whole program has only one flow of execution, so at most one handler is running at any instant. Shared data cannot be accessed concurrently, therefore mutexes, condition variables and the deadlocks and races they introduce all disappear. The programmer can concentrate on the application logic instead of on synchronization details.

A Blocking Call Freezes the Whole Loop

The moment any handler issues a system call that must wait for disk or network, the loop stops there and every later event goes unserved. The only way to stay responsive is to make every I/O operation a non-blocking interface that returns immediately; the real wait happens in the background and is later delivered to the loop as a fresh event.

Pitfalls

  • Calling a blocking open or read inside a handler makes the entire server unresponsive
  • Failing to inspect every ready descriptor after select misses events

Run an example

Minimum C11 · complete program · Download .c

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    bool ready[2][4] = {
        {true, false, true, false},
        {false, true, false, true}
    };
    const char *names[4] = {"listen", "clientA", "clientB", "disk"};
    printf("Starting simulated event loop\n");
    for (int round = 0; round < 2; round++) {
        printf("Round %d:\n", round + 1);
        for (int fd = 0; fd < 4; fd++) {
            if (ready[round][fd]) {
                printf("  Handling event on %s (fd %d)\n", names[fd], fd);
            }
        }
    }
    printf("Event loop finished without blocking\n");
    return 0;
}

Compile locally

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-33-event-based.c -o example && ./example

Expected result

Starting simulated event loop
Round 1:
  Handling event on listen (fd 0)
  Handling event on clientB (fd 2)
Round 2:
  Handling event on clientA (fd 1)
  Handling event on disk (fd 3)
Event loop finished without blocking

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Why does an event-driven server on a single CPU need no locks when touching shared data?

Show a reference answer

Because only one handler executes at a time, two flows of execution can never read or write the same data simultaneously.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog