# How to Integrate ASIO with Custom Event Loops: Two Proven Patterns

> Learn how to integrate ASIO with custom event loops using two proven patterns. Explore polling io_context or implementing custom Executors for seamless integration.

- Repository: [chriskohlhoff/asio](https://github.com/chriskohlhoff/asio)
- Tags: how-to-guide
- Published: 2026-07-12

---

**You can integrate ASIO with custom event loops by either polling the `io_context` inside your own loop using `poll_one()` or `run_one()`, or by implementing a custom Executor that forwards ASIO completion handlers to your scheduler.**

The chriskohlhoff/asio library decouples asynchronous I/O operations from the mechanism that runs them, making it possible to embed its networking and timer facilities into existing applications. To integrate ASIO with custom event loops, you interact with the `io_context` class defined in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp) and leverage the lightweight Executor concept found in [`include/asio/executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/executor.hpp). This architecture allows ASIO to coexist with game engines, GUI frameworks, or proprietary reactors without forcing you to abandon your existing threading model.

## Poll-in-Your-Loop Pattern

The simplest way to integrate ASIO with custom event loops is to embed an `io_context` inside your existing event loop and manually drive its reactor.

### When to Use This Approach

Use this pattern when you already have a main loop—such as a game loop or a GUI message pump—and you want ASIO to cooperate without taking control of the thread. Because the loop is exposed as ordinary member functions, you can call `io_context::poll_one()` or `io_context::run_one()` at each iteration to process ready handlers and immediately return to your surrounding code.

### Implementation Details

The `io_context` class in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp) provides several non-blocking methods:

- `poll()` – executes all ready handlers without blocking.
- `poll_one()` – executes at most one ready handler without blocking.
- `run_one()` – blocks until at least one handler executes, then returns.

For a cooperative loop, `poll_one()` is typically preferred because it returns immediately if no work is ready, allowing your loop to continue processing other events.

### Example: Embedding ASIO in a Game Loop

```cpp
// custom_loop.cpp
#include <asio.hpp>
#include <iostream>

int main() {
    asio::io_context ctx;

    // Example async timer that fires after 1 s
    asio::steady_timer t(ctx, std::chrono::seconds(1));
    t.async_wait([](const asio::error_code& ec) {
        if (!ec) std::cout << "Timer expired!\n";
    });

    // Your own event loop (e.g. a game loop)
    for (int frame = 0; frame < 100; ++frame) {
        // ...do your per-frame work here...

        // Process any ready ASIO handlers without blocking
        ctx.poll_one();          // or ctx.run_one(); if you prefer blocking a little
    }
}

```

In this example, the call to `ctx.poll_one()` in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp) drains any ready ASIO handlers (the timer in this case) and immediately returns control to the surrounding loop, ensuring your frame processing continues uninterrupted.

## Custom Executor Pattern

For deeper integration, you can implement a custom Executor that satisfies the **Executor** concept defined in [`include/asio/executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/executor.hpp). This allows ASIO to post completion handlers directly onto your own scheduler, such as a task queue or thread pool.

### Implementing the Executor Concept

According to the ASIO source code in [`include/asio/executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/executor.hpp), a type satisfies the Executor concept if it provides an `execute(F)` function that accepts a callable, along with optional `context()` and `blocking` traits. ASIO uses this interface via `asio::any_io_executor` defined in [`include/asio/any_io_executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/any_io_executor.hpp) to type-erase and forward work.

### Thread Pool Executor Example

```cpp
// custom_executor.cpp
#include <asio.hpp>
#include <queue>
#include <mutex>
#include <thread>
#include <iostream>

// A very simple thread-pool executor
class thread_pool_executor {
public:
    thread_pool_executor(std::size_t n) : stop_(false) {
        for (std::size_t i = 0; i < n; ++i)
            workers_.emplace_back([this] { this->worker_thread(); });
    }
    ~thread_pool_executor() {
        {
            std::lock_guard<std::mutex> lk(m_);
            stop_ = true;
        }
        cv_.notify_all();
        for (auto& th : workers_) th.join();
    }

    // Executor requirement
    template<class F>
    void execute(F f) const {
        {
            std::lock_guard<std::mutex> lk(m_);
            tasks_.push(std::function<void()>(f));
        }
        cv_.notify_one();
    }

private:
    void worker_thread() {
        while (true) {
            std::function<void()> work;
            {
                std::unique_lock<std::mutex> lk(m_);
                cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); });
                if (stop_ && tasks_.empty()) return;
                work = std::move(tasks_.front());
                tasks_.pop();
            }
            work();
        }
    }

    mutable std::mutex m_;
    mutable std::condition_variable cv_;
    std::queue<std::function<void()>> tasks_;
    std::vector<std::thread> workers_;
    bool stop_;
};

```

Here, `thread_pool_executor` satisfies the Executor concept by providing the `execute(F)` method. The `asio::post` function in [`include/asio/post.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/post.hpp) and `asio::bind_executor` use this interface to forward work to your custom scheduler.

### Binding Custom Executors to ASIO Operations

Once you have a custom executor, you can bind it to specific operations using `asio::bind_executor`:

```cpp
int main() {
    asio::io_context ctx;
    thread_pool_executor exec(4);            // 4-thread pool

    // Bind the custom executor to all subsequent async operations
    asio::post(exec, []{ std::cout << "Hello from custom executor!\n"; });

    // Example async read using the bound executor
    asio::steady_timer t(ctx, std::chrono::seconds(1));
    t.async_wait(
        asio::bind_executor(exec,
            [](const asio::error_code& ec) {
                if (!ec) std::cout << "Timer fired on custom executor\n";
            })
    );

    // Run the ASIO context (it will only drive the timer)
    ctx.run();
}

```

In this pattern, ASIO still requires an `io_context` to drive the underlying reactor (timers, sockets), but the completion handlers execute on your custom thread pool instead of the `io_context`'s internal threads.

### Co-Spawn with Coroutines

If you use C++20 coroutines, you can launch `asio::awaitable` tasks directly onto your custom executor using `asio::co_spawn`, defined in [`include/asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/awaitable.hpp):

```cpp
#include <asio.hpp>
#include <iostream>

template<class Executor>
asio::awaitable<void> periodic_task(Executor exec) {
    auto timer = asio::steady_timer(co_await asio::this_coro::executor);
    while (true) {
        timer.expires_after(std::chrono::seconds(2));
        co_await timer.async_wait(asio::use_awaitable);
        std::cout << "Tick on custom executor\n";
    }
}

int main() {
    asio::io_context ctx;
    thread_pool_executor exec(2);

    // Co-spawn the coroutine onto the custom executor
    asio::co_spawn(exec,
        periodic_task(exec),
        asio::detached);      // fire-and-forget
    ctx.run();                // drives the timer's internal reactor
}

```

Because `asio::co_spawn` accepts any executor satisfying the concept, the coroutine’s resumption—including its timer waits—will be scheduled onto the `thread_pool_executor`.

## Key ASIO Components for Custom Integration

Understanding these core components from the chriskohlhoff/asio source code is essential for successful integration:

- **`io_context`** ([`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp)): The central hub that owns the reactor and stores work. It provides `run()`, `run_one()`, `poll()`, and `poll_one()` to drive the event loop.
- **`any_io_executor`** ([`include/asio/any_io_executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/any_io_executor.hpp)): A type-erased wrapper that holds any executor satisfying the Executor concept, used by most ASIO APIs to schedule work.
- **Executor Concept** ([`include/asio/executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/executor.hpp)): Defines the `execute(F)` requirement and associated traits that allow custom schedulers to participate in ASIO's handler dispatch mechanism.
- **`post` and `dispatch`** ([`include/asio/post.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/post.hpp)): Helper functions that forward callables to the associated executor, respecting the executor's scheduling guarantees.
- **`awaitable` and `co_spawn`** ([`include/asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/awaitable.hpp)): Coroutine support that allows asynchronous operations to be suspended and resumed on a custom executor.

## Summary

- **Poll-in-your-loop**: Call `io_context::poll_one()` or `run_one()` inside your existing event loop to process ASIO handlers cooperatively without yielding thread control.
- **Custom Executor**: Implement an `execute(F)` method satisfying the Executor concept to forward ASIO handlers to your own thread pool or task queue, using `asio::bind_executor` or `asio::post` to route work.
- **Coroutine Support**: Use `asio::co_spawn` with your custom executor to run C++20 coroutines on your scheduler while letting `io_context::run()` drive the underlying reactor.

## Frequently Asked Questions

### What is the difference between `poll_one()` and `run_one()`?

`poll_one()` checks for ready handlers and executes at most one, returning immediately if none are available, making it ideal for non-blocking integration. `run_one()` blocks the calling thread until at least one handler becomes ready and executes it, which is useful when your loop can tolerate brief waits for ASIO events.

### How do I integrate ASIO with a GUI framework like Qt or GLFW?

Use the **poll-in-your-loop** pattern. In your GUI's main event loop (e.g., `QApplication::processEvents()` or the GLFW render loop), call `io_context::poll_one()` each iteration. This processes network events and timers without blocking the UI thread, ensuring responsive interfaces while ASIO performs I/O in the background.

### Can I use C++20 coroutines with a custom executor?

Yes. The `asio::co_spawn` function in [`include/asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/awaitable.hpp) accepts any type satisfying the Executor concept. When you spawn a coroutine onto a custom executor, every suspension point (such as `co_await` on an async operation) will resume on that executor, allowing you to control which thread pool handles the coroutine's logic.

### What are the minimum requirements for a custom Executor type?

According to [`include/asio/executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/executor.hpp), a custom executor must provide an `execute(F)` function that accepts a callable and invokes it. Optionally, you can define `context()` to return an execution context and `blocking` to specify blocking behavior, though ASIO provides reasonable defaults via the `executor` traits. The `asio::any_io_executor` class in [`include/asio/any_io_executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/any_io_executor.hpp) type-erases these requirements for use in ASIO APIs.