The whole concurrency Part so far assumed one tool: threads. But they arenβt the only way. Event-based concurrency event-based concurrency A style of concurrent programming that uses no threads: a single-threaded event loop waits for events and dispatches each to a handler, one at a time. It gives the application explicit control over scheduling and needs no locks (on one CPU), at the cost of complexity around blocking calls and manual state management. Used in GUIs, servers like Flash, and node.js. defined in ch. 33 β open in glossary β 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 event loop The heart of an event-based server: while(1){ events = getEvents(); for (e in events) processEvent(e); }. It waits for events, then processes each one with its handler, one at a time. Because only one handler runs at a time, choosing which event to handle next IS scheduling. defined in ch. 33 β open in glossary :
while (1) {
events = getEvents();
for (e in events)
processEvent(e);
}
The code handling each event is an event handler event handler The code that processes a single event in an event-based server. Since handlers run one at a time in a single thread, no locks are needed β but a handler must never make a blocking call, or the whole server stalls. defined in ch. 33 β open in glossary . 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:
1Wait in getEvents()
The loop calls getEvents() (via select()) and BLOCKS, waiting for any watched socket to have data. Nothing else runs.
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 non-blocking A non-blocking (asynchronous) interface begins some work but returns to the caller immediately, letting the work finish in the background β versus a blocking (synchronous) interface that does all its work before returning. Non-blocking calls are essential to event-based servers, since any blocking call would halt the single-threaded event loop. defined in ch. 33 β open in glossary (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:
1FD_ZERO(&readFDs) β clear
Start each loop iteration by clearing the descriptor set: no descriptors of interest yet.
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 | events | |
|---|---|---|
| Who schedules? | the OS (you hope it's reasonable) | YOU β pick the next event |
| Locks? | needed β races & deadlock lurk | none (one handler at a time) |
| Blocking calls? | fine β other threads run | FORBIDDEN β halts the server |
| Multiple CPUs? | natural β threads run in parallel | loses the no-lock simplicity |
| State across I/O? | automatic β on the thread's stack | manual β package a continuation |
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?