Fundamentals 13 min read

Why a Single‑Threaded Event Loop Can Beat Multithreading: Exploring Coroutines and the Reactor Pattern

This article walks through serial file reading, thread‑based parallelism, event‑driven asynchronous I/O, callbacks, and finally shows how coroutines combined with an event loop provide a synchronous‑style solution that scales efficiently without the drawbacks of massive threading.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Why a Single‑Threaded Event Loop Can Beat Multithreading: Exploring Coroutines and the Reactor Pattern

Serial Approach – The Simplest Method

The most straightforward way to read ten files is to process them one after another, which is easy to understand and maintain but incurs a total time equal to the sum of each file's read duration.

for file in files:
    result = file.read()
    process(result)

Advantages: simple code, easy maintenance. Drawback: it is slow because I/O operations are performed sequentially.

Code is simple and easy to understand.

Maintainability is high; any developer can work on it.

Threaded Parallelism – A Better Method

Launching a separate thread for each file allows concurrent reads, reducing total time dramatically when the number of files is small.

def read_and_process(file):
    result = file.read()
    process(result)

def main():
    files = [fileA, fileB, fileC, ...]
    for file in files:
        create_thread(read_and_process, file).run()
    # wait for all threads to finish

While this works for ten files, scaling to thousands of files creates problems: each thread consumes system resources, scheduling overhead grows, and I/O devices can become saturated, negating performance gains.

Thread creation consumes memory and other resources.

Heavy scheduling overhead when many threads are busy.

When the I/O subsystem is saturated, adding threads does not speed up processing.

Event‑Driven Asynchronous I/O – Parallelism Without Threads

Using an event loop, a program can issue many non‑blocking I/O requests from a single thread and later react when each operation completes.

event_loop = EventLoop()

def add_to_event_loop(event_loop, file):
    file.async_read()          # start asynchronous read, returns immediately
    event_loop.add(file)

while event_loop:
    file = event_loop.wait_one_IO_ready()
    process(file.result)

The call to file.async_read() returns instantly, so the program can launch all ten reads instantly; the event loop then waits for the first I/O to finish. Because the other nine reads finish during that wait, the total execution time is roughly the duration of a single read.

Callbacks – Adding Flexibility at a Cost

To handle different I/O types, callbacks can be registered alongside the I/O request.

def IO_type_1(event_loop, io):
    io.start()
    def callback(result):
        process_IO_type_1(result)
    event_loop.add((io, callback))

while event_loop:
    io, callback = event_loop.wait_one_IO_ready()
    callback(io.result)

This keeps the event loop simple, but as the number of I/O types grows the code can become tangled, leading to the classic “callback hell” where callbacks are nested inside callbacks, making maintenance difficult.

Coroutines – Synchronous‑Style Asynchronous Programming

Coroutines let a programmer pause execution at a chosen point, yielding control back to the event loop, and later resume exactly where it left off, eliminating the need for separate callbacks.

def start_IO_type_1(io):
    io.start()          # issue asynchronous I/O request
    yield               # suspend coroutine until I/O completes
    process_IO_type_1(result)

def add_to_event_loop(io, event_loop):
    coroutine = start_IO_type_1(io)
    next(coroutine)     # run until first yield
    event_loop.add(coroutine)

while event_loop:
    coroutine = event_loop.wait_one_IO_ready()
    next(coroutine)    # resume after I/O is ready

The code now reads like ordinary sequential code, yet the underlying I/O remains non‑blocking. The event loop is still required to monitor I/O completion, but coroutines remove the callback indirection and avoid the pitfalls of callback hell.

Reactor Pattern – The Underlying Model

The combination of an event loop, asynchronous I/O, and callbacks (or coroutines) is known as the Reactor pattern: the program registers interest in events, the reactor notifies when they occur, and the registered handler processes them. This pattern powers many high‑performance servers and frameworks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

asynchronous programmingReactor Patterncoroutineevent loopio concurrency
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.