Unlocking the Power of Single-Threaded Processes: Event Loops, Epoll, and io_uring

Dishank Oza
4 min readJan 13, 2025

This week, I stumbled upon an insightful article about how io_uring solves traditional I/O problems using I/O multiplexing. It took me down memory lane to my bachelor’s days when I implemented a basic client-server program. Back then, I faced a classic issue: when one client connected to the server, it blocked other connections. To address this, we implemented multithreading, where each new client connection spun up a new thread.

While functional, this approach isn’t scalable. As the number of connections grows, the server spins up too many threads, consuming resources and leading to thread management overhead. This is where the magic of I/O multiplexing and single-threaded event-driven architectures comes in. Let’s dive deeper into this paradigm shift and how it powers modern systems like Redis, JavaScript.

The Problem with Blocking and Multithreading

In a basic blocking server:

  • The server listens for incoming client connections.
  • When a client connects, the server blocks while reading data from or writing data to the client.
  • Other clients must wait until the current client’s request is processed.

To solve this, multithreading was introduced:

  • Each new connection spawns a thread to handle the client.
  • While effective for low traffic, as connections grow, the system struggles due to thread context switching, memory usage, and CPU overhead.

Clearly, this isn’t scalable for modern applications.

The Solution: Single Thread with I/O Multiplexing

I/O multiplexing enables a single-threaded server to handle multiple client connections concurrently by monitoring multiple file descriptors (FDs) for events. Key concepts:

  • File Descriptors (FDs): Each client connection is represented as an FD
  • Event Loop: A loop continuously checks which FDs are ready for I/O operations and processes them

Epoll: An Event-Driven I/O Multiplexing System

Linux introduced epoll, a highly efficient mechanism for I/O multiplexing:

  • epoll_create: Creates an epoll instance.
  • epoll_ctl: Registers FDs (e.g., client sockets) to monitor.
  • epoll_wait: Waits for events on the registered FDs and processes them.

The beauty of epoll lies in its event-driven model:

  • The server doesn’t block while waiting for I/O.
  • Instead, it processes FDs only when they are “ready” (e.g., data is available to read).

How Redis Leverages Epoll

Redis, a single-threaded, event-driven database, uses epoll to handle thousands of client connections. It doesn’t need complex locking mechanisms because only one operation executes at a time. Redis reads commands only when a client sends data and uses epoll to monitor multiple client FDs efficiently.

The Event Loop in JavaScript

JavaScript’s event loop is a prime example of how single-threaded architectures handle asynchronous I/O:

  • The call stack processes synchronous code.
  • Asynchronous tasks (e.g., disk I/O, network requests) are offloaded to background threads.
  • Once an asynchronous task completes, its callback is queued in the event queue.
  • The event loop moves tasks from the event queue to the call stack when the stack is empty.

This allows JavaScript to handle asynchronous tasks without blocking the main thread.

Implementing Event Loop in Go

Inspired by Redis and the JavaScript event loop, I created a repository to demonstrate how a single-threaded TCP server benefits from I/O multiplexing. Here’s a breakdown:

Synchronous TCP Server

In the synchronous model:

  • Each connection blocks the server until the request is fully processed.
  • This is inefficient for handling multiple clients.

Asynchronous Server with Goroutines

Using Go’s goroutines, we can handle multiple clients concurrently. However, each connection spawns a lightweight goroutine, which can still lead to resource exhaustion with a large number of connections.

Asynchronous Server with Epoll

Using syscalls and epoll, we can efficiently manage multiple client connections in a single thread.

How It Works

  1. Create a Socket: Using syscall.Socket to create a non-blocking TCP socket.
  2. Bind and Listen: Bind the socket to a port and start listening for incoming connections.
  3. Create Epoll Instance: Use syscall.EpollCreate1 to create an epoll instance.
  4. Register Events: Register the server socket with epoll to monitor for incoming connections.
  5. Event Loop:
  • Use syscall.EpollWait to wait for events on registered FDs.
  • For each event:
  1. If it’s the server FD, accept a new connection and register the client FD with epoll.
  2. If it’s a client FD, read incoming data and send a response.

Code Walkthrough

You can find the complete implementation in my repo: echo-server. Here’s a snippet of the event loop:

for {
nevents, err := syscall.EpollWait(epollFD, events, -1)
if err != nil {
continue
} for i := 0; i < nevents; i++ {
if int(events[i].Fd) == serverFD {
fd, _, err := syscall.Accept(serverFD)
if err == nil {
syscall.SetNonblock(fd, true)
epollEvent := syscall.EpollEvent{Fd: int32(fd), Events: syscall.EPOLLIN}
syscall.EpollCtl(epollFD, syscall.EPOLL_CTL_ADD, fd, &epollEvent)
}
} else {
// Read and respond to client
buffer := make([]byte, 512)
n, err := syscall.Read(int(events[i].Fd), buffer)
if err != nil || n == 0 {
syscall.Close(int(events[i].Fd))
continue
}
syscall.Write(int(events[i].Fd), buffer[:n])

Conclusion

Amazing how one concept is used in many places with such a simple implementation! Using single-threaded architectures with event-driven I/O multiplexing like epoll offers a scalable solution for handling thousands of connections. It minimizes resource usage while maintaining high throughput. Tools like io_uring push this further, reducing system call overhead.

Check out my implementation and let me know in the comments if you’d like a deeper dive into the code or concepts!

I am currently looking for a summer internship and would love to connect. Feel free to reach out — I’d love to share my experience and learn from yours!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Dishank Oza
Dishank Oza

No responses yet

Write a response