Β§33.1–33.4The Basic Idea: An Event Loop … Why Simpler? No Locks Needed

Part II OSTEP pp. 401–404 Β· ~7 min read

  • event-based concurrency
  • event loop
  • event handler
  • non-blocking

The whole concurrency Part so far assumed one tool: threads. But they aren’t the only way. Event-based concurrency β€” used in GUIs, high-performance servers, and node.js β€” takes a different tack, fixing two thread headaches: getting locking correct is hard, and you have little control over scheduling.

The Crux: How To Build Concurrent Servers Without Threads

How can we build a concurrent server without threads β€” keeping control over concurrency and dodging the problems that plague multi-threaded programs?

33.1 The Basic Idea: An Event Loop

Wait for something (an event) to happen; when it does, figure out what it is and do the small bit of work it needs. The whole server is built around an event loop :

while (1) {
    events = getEvents();
    for (e in events)
        processEvent(e);
}

The code handling each event is an event handler . The key insight: while a handler runs, it is the only activity in the system β€” so deciding which event to handle next is exactly scheduling. Watch one turn of the loop:

The event loop: wait for events, then handle each one β€” alone β€” before looping back. You control the order, so you control scheduling.
single-threaded event loopwhile(1){ events=getEvents(); for(e) processEvent(e); }ready events:(none β€” blocked in getEvents(), waiting)πŸ”“ one handler at a time β†’ no locks needed

1Wait in getEvents()

The loop calls getEvents() (via select()) and BLOCKS, waiting for any watched socket to have data. Nothing else runs.

step 1 / 5

33.2 An Important API: select() (or poll())

How does the loop learn which events are ready β€” say, which sockets have an incoming packet? The select() (or poll()) system call:

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *errorfds, struct timeval *timeout);

It examines sets of file descriptors and reports which are ready to read (a packet arrived), write (OK to reply), or have an error. timeout = NULL blocks until something is ready; timeout = 0 polls and returns immediately.

Aside: Blocking vs. Non-Blocking Interfaces

A blocking (synchronous) call does all its work before returning; a non-blocking (asynchronous) call begins work and returns immediately, finishing in the background. The usual blocker is I/O. Non-blocking calls can be used anywhere, but are essential to event-based servers β€” a blocking call would halt all progress.

33.3 Using select()

Fig 33.1: each iteration clears the set (FD_ZERO), marks the descriptors of interest (FD_SET), calls select(), then checks which came back ready (FD_ISSET) and processes them:

while (1) {
    fd_set readFDs;
    FD_ZERO(&readFDs);                       // clear the set
    for (int fd = minFD; fd < maxFD; fd++)
        FD_SET(fd, &readFDs);                // mark interest

    select(maxFD+1, &readFDs, NULL, NULL, NULL);  // which are ready?

    for (int fd = minFD; fd < maxFD; fd++)
        if (FD_ISSET(fd, &readFDs))
            processFD(fd);                   // handle the ready ones
}

Step through what happens to the descriptor set:

select() at work: clear the set, mark the descriptors you care about, then let select() report which are ready.
fd_set readFDs:0fd 30fd 40fd 50fd 60fd 7

1FD_ZERO(&readFDs) β€” clear

Start each loop iteration by clearing the descriptor set: no descriptors of interest yet.

step 1 / 3

Tip: Don’t Block In Event-Based Servers

Event-based servers give fine-grained control over scheduling β€” but to keep it, no call that blocks the caller may ever be made. Break this rule and you get a blocked server, frustrated clients, and serious doubts about whether you read this part of the book.

33.4 Why Simpler? No Locks Needed

On a single CPU, the event-based approach makes concurrency bugs vanish. Because only one event is handled at a time, and the loop can’t be interrupted by another thread (it’s decidedly single-threaded), there’s no need to acquire or release locks β€” the races and deadlocks of the threaded world simply don’t manifest. Threads and events, side by side:

Threads vs. events β€” two answers to the same concurrency problem.
threadsevents
Who schedules?the OS (you hope it's reasonable)YOU β€” pick the next event
Locks?needed β€” races & deadlock lurknone (one handler at a time)
Blocking calls?fine β€” other threads runFORBIDDEN β€” halts the server
Multiple CPUs?natural β€” threads run in parallelloses the no-lock simplicity
State across I/O?automatic β€” on the thread's stackmanual β€” package a continuation
Dotted-underlined cells have explanations β€” click one.

That β€œno locks” column is the event model’s headline win β€” but the β€œblocking calls FORBIDDEN” row is its Achilles’ heel, and the subject of the next section.

Check yourself: the event loop

1.What does an event-based server's main loop do?

2.Why does the basic event-based approach need no locks (on a single CPU)?

3.What does the select() system call do for an event loop?

4.What's the difference between a blocking and a non-blocking interface?

5.Why is 'deciding which event to handle next' considered a fundamental advantage of the event model?

6.What is the central rule (and Achilles' heel) of event-based servers?

6 questions