Skip to content

Async I/O Deep Dive Engine

The Engine Underneath: Hiểu cách Event Loop hoạt động — nền tảng của Nginx, Node.js, và mọi high-performance server.

Blocking vs Non-Blocking I/O

Blocking I/O (Traditional)

cpp
// ❌ BLOCKING - Thread bị block khi đợi data
char buffer[1024];
int bytes = recv(socket, buffer, sizeof(buffer), 0);
// Thread STOPS here until data arrives (could be seconds!)
process(buffer, bytes);
┌─────────────────────────────────────────────────────────────────────────┐
│                    BLOCKING I/O TIMELINE                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Thread 1:  ┌──────┐          ┌──────────────────────┐   ┌────────┐   │
│              │ Work │ ──────── │ WAITING for recv()   │ ─ │ Process│   │
│              └──────┘          └──────────────────────┘   └────────┘   │
│                                        ↑                               │
│                                    WASTED TIME                         │
│                                   (thread idle)                        │
│                                                                         │
│   1000 connections = 1000 threads = 1000 × wasted time = 💀            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Non-Blocking I/O (Modern)

cpp
// ✅ NON-BLOCKING - Check and return immediately
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, flags | O_NONBLOCK);

char buffer[1024];
int bytes = recv(socket, buffer, sizeof(buffer), 0);
if (bytes == -1 && errno == EAGAIN) {
    // No data yet, do something else
    // Event loop will notify when ready
}
┌─────────────────────────────────────────────────────────────────────────┐
│                    NON-BLOCKING I/O TIMELINE                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Single Thread handling 1000 connections:                              │
│                                                                         │
│   ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │
│   │Conn 1│ │Conn 5│ │Conn 3│ │Conn 7│ │Conn 2│ │Conn 8│ │Conn 1│ ...  │
│   └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘      │
│                                                                         │
│   Event Loop: "Only process connections with ready data"               │
│   → No wasted time, CPU always doing useful work                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

The Event Loop — Visualized

┌─────────────────────────────────────────────────────────────────────────┐
│                    EVENT LOOP ARCHITECTURE                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│                         ┌─────────────────┐                             │
│                         │   EVENT LOOP    │                             │
│                         │   (io_context)  │                             │
│                         └────────┬────────┘                             │
│                                  │                                      │
│                    ┌─────────────┼─────────────┐                        │
│                    ▼             ▼             ▼                        │
│              ┌──────────┐  ┌──────────┐  ┌──────────┐                   │
│              │  Timer   │  │  Socket  │  │  Signal  │                   │
│              │  Events  │  │  Events  │  │  Events  │                   │
│              └──────────┘  └──────────┘  └──────────┘                   │
│                    │             │             │                        │
│                    └─────────────┼─────────────┘                        │
│                                  ▼                                      │
│                         ┌─────────────────┐                             │
│                         │  OS Kernel      │                             │
│                         │  (epoll/kqueue) │                             │
│                         └─────────────────┘                             │
│                                                                         │
│   Loop Flow:                                                            │
│   ┌─────────────────────────────────────────────────────────────┐      │
│   │  1. Wait for events (epoll_wait)                            │      │
│   │  2. Dispatch ready handlers                                  │      │
│   │  3. Execute handlers (callbacks)                             │      │
│   │  4. Go to step 1                                             │      │
│   └─────────────────────────────────────────────────────────────┘      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Boost.Asio — Industry Standard

Boost.Asio là thư viện async I/O mạnh nhất cho C++, được sử dụng trong:

  • gRPC C++ implementation
  • Beast (HTTP/WebSocket)
  • Crow, Drogon (web frameworks)
  • HPN Tunnel

Installation

bash
# Via Conan
conan install boost/1.83.0@

# Via Vcpkg
vcpkg install boost-asio

# Ubuntu
sudo apt install libboost-all-dev

CMake Integration

cmake
find_package(Boost REQUIRED COMPONENTS system)
target_link_libraries(myapp PRIVATE Boost::system)

Asio Basics: io_context

io_context là trung tâm của Asio — Event Loop.

cpp
#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

int main() {
    // Create the event loop
    asio::io_context io;
    
    // Post work to the event loop
    io.post([]() {
        std::cout << "Hello from event loop!" << std::endl;
    });
    
    io.post([]() {
        std::cout << "Another task!" << std::endl;
    });
    
    // Run the event loop (blocks until no more work)
    io.run();
    
    return 0;
}

Timer Example

cpp
#include <boost/asio.hpp>
#include <iostream>
#include <chrono>

namespace asio = boost::asio;
using namespace std::chrono_literals;

int main() {
    asio::io_context io;
    
    // Create a timer
    asio::steady_timer timer(io, 2s);
    
    // Async wait - doesn't block!
    timer.async_wait([](const boost::system::error_code& ec) {
        if (!ec) {
            std::cout << "Timer expired!" << std::endl;
        }
    });
    
    std::cout << "Timer started, doing other work..." << std::endl;
    
    // This runs until timer callback completes
    io.run();
    
    return 0;
}

TCP Echo Server (Callback Style)

cpp
#include <boost/asio.hpp>
#include <iostream>
#include <memory>

namespace asio = boost::asio;
using asio::ip::tcp;

class Session : public std::enable_shared_from_this<Session> {
public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}
    
    void start() {
        do_read();
    }
    
private:
    void do_read() {
        auto self = shared_from_this();
        
        socket_.async_read_some(
            asio::buffer(data_, max_length),
            [this, self](boost::system::error_code ec, 
                         std::size_t length) {
                if (!ec) {
                    do_write(length);
                }
            });
    }
    
    void do_write(std::size_t length) {
        auto self = shared_from_this();
        
        asio::async_write(
            socket_,
            asio::buffer(data_, length),
            [this, self](boost::system::error_code ec, 
                         std::size_t /*length*/) {
                if (!ec) {
                    do_read();  // Continue reading
                }
            });
    }
    
    tcp::socket socket_;
    enum { max_length = 1024 };
    char data_[max_length];
};

class Server {
public:
    Server(asio::io_context& io, short port)
        : acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
        do_accept();
    }
    
private:
    void do_accept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    std::make_shared<Session>(std::move(socket))->start();
                }
                do_accept();  // Accept next connection
            });
    }
    
    tcp::acceptor acceptor_;
};

int main() {
    try {
        asio::io_context io;
        Server server(io, 8080);
        
        std::cout << "Server running on port 8080" << std::endl;
        io.run();
        
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
}

C++20 Coroutines (Modern Approach)

Callbacks = "Callback Hell". Coroutines = Clean, linear code.

cpp
// C++20 with Asio coroutines
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;

awaitable<void> echo(tcp::socket socket) {
    try {
        char data[1024];
        
        for (;;) {
            // co_await - suspend until data ready
            std::size_t n = co_await socket.async_read_some(
                asio::buffer(data), use_awaitable);
            
            // co_await - suspend until write complete
            co_await async_write(socket, 
                                 asio::buffer(data, n), 
                                 use_awaitable);
        }
    } catch (std::exception& e) {
        std::cout << "Connection closed: " << e.what() << std::endl;
    }
}

awaitable<void> listener() {
    auto executor = co_await asio::this_coro::executor;
    tcp::acceptor acceptor(executor, {tcp::v4(), 8080});
    
    for (;;) {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
        co_spawn(executor, echo(std::move(socket)), detached);
    }
}

int main() {
    try {
        asio::io_context io;
        
        co_spawn(io, listener(), detached);
        
        io.run();
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
}

Comparison: Callbacks vs Coroutines

┌─────────────────────────────────────────────────────────────────────────┐
│                    CALLBACKS vs COROUTINES                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   CALLBACKS (Callback Hell):                                            │
│   ─────────────────────────                                             │
│   async_read([](ec, n) {                                                │
│       if (!ec) {                                                        │
│           async_write([](ec, n) {                                       │
│               if (!ec) {                                                │
│                   async_read([](ec, n) {                                │
│                       // ... nested forever                             │
│                   });                                                   │
│               }                                                         │
│           });                                                           │
│       }                                                                 │
│   });                                                                   │
│                                                                         │
│   COROUTINES (Linear, clean):                                           │
│   ───────────────────────────                                           │
│   auto n = co_await async_read(use_awaitable);                          │
│   co_await async_write(use_awaitable);                                  │
│   n = co_await async_read(use_awaitable);                               │
│   // Reads like synchronous code!                                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Multi-threaded Event Loop

cpp
#include <boost/asio.hpp>
#include <thread>
#include <vector>

int main() {
    asio::io_context io;
    
    // Prevent io_context from stopping when no work
    auto work_guard = asio::make_work_guard(io);
    
    // Create thread pool
    std::vector<std::thread> threads;
    const auto thread_count = std::thread::hardware_concurrency();
    
    for (unsigned i = 0; i < thread_count; ++i) {
        threads.emplace_back([&io]() {
            io.run();  // Each thread runs the same io_context
        });
    }
    
    // ... add your async operations ...
    
    // Shutdown
    work_guard.reset();  // Allow io_context to stop
    for (auto& t : threads) {
        t.join();
    }
}
┌─────────────────────────────────────────────────────────────────────────┐
│                    MULTI-THREADED IO_CONTEXT                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│                      ┌─────────────────┐                                │
│                      │   io_context    │                                │
│                      │   (Event Loop)  │                                │
│                      └────────┬────────┘                                │
│                               │                                         │
│           ┌───────────────────┼───────────────────┐                     │
│           ▼                   ▼                   ▼                     │
│   ┌────────────────┐  ┌────────────────┐  ┌────────────────┐           │
│   │   Thread 1     │  │   Thread 2     │  │   Thread N     │           │
│   │   io.run()     │  │   io.run()     │  │   io.run()     │           │
│   └────────────────┘  └────────────────┘  └────────────────┘           │
│                                                                         │
│   All threads share the same io_context                                 │
│   Handlers may run on ANY thread → Use strand for serialization         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Strand for Thread Safety

cpp
// strand ensures handlers are serialized (not concurrent)
asio::strand<asio::io_context::executor_type> strand(
    asio::make_strand(io));

// All handlers on this strand run sequentially
asio::post(strand, []() {
    // This handler...
});

asio::post(strand, []() {
    // ...and this handler will NEVER run concurrently
});

OS-Level Event Notification

┌─────────────────────────────────────────────────────────────────────────┐
│                    OS EVENT NOTIFICATION MECHANISMS                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Linux:    epoll                                                       │
│   ─────────────────                                                     │
│   • O(1) event notification                                             │
│   • Edge-triggered or Level-triggered                                   │
│   • Scales to millions of connections                                   │
│                                                                         │
│   macOS/BSD: kqueue                                                     │
│   ─────────────────                                                     │
│   • Similar to epoll                                                    │
│   • Supports file, socket, signal, timer events                         │
│                                                                         │
│   Windows:  IOCP (I/O Completion Ports)                                 │
│   ───────────────────────────────────────                               │
│   • Proactor model (completion-based)                                   │
│   • Kernel handles more work                                            │
│                                                                         │
│   Linux 5.1+: io_uring                                                  │
│   ─────────────────────                                                 │
│   • Newest, fastest mechanism                                           │
│   • Kernel bypass possible                                              │
│   • Used in cutting-edge systems                                        │
│                                                                         │
│   Boost.Asio abstracts ALL of these! Write once, run anywhere.          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Best Practices

┌─────────────────────────────────────────────────────────────────────────┐
│                    ASYNC I/O BEST PRACTICES                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ✅ DO                                                                  │
│   ─────                                                                 │
│   • Use shared_ptr with enable_shared_from_this for session lifetime    │
│   • Use strand for thread-safe handler execution                        │
│   • Prefer coroutines (C++20) over raw callbacks                        │
│   • Pre-allocate buffers to avoid allocation in hot path               │
│   • Use work_guard to prevent premature io_context exit                 │
│                                                                         │
│   ❌ DON'T                                                               │
│   ───────                                                               │
│   • Don't block inside handlers (defeats the purpose!)                  │
│   • Don't forget error handling (always check error_code)               │
│   • Don't share mutable state without synchronization                   │
│   • Don't use global io_context (prefer dependency injection)           │
│                                                                         │
│   🏎️ PERFORMANCE TIPS                                                   │
│   ─────────────────────                                                 │
│   • Thread count = CPU cores (not more!)                                │
│   • Edge-triggered epoll for very high throughput                       │
│   • Object pool for Session objects (avoid new/delete)                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Bước tiếp theo

Đã hiểu async engine, giờ học production patterns:

🛡️ Production Patterns → — Zero-copy, Load testing, TLS, Security