# How to Perform Non-Blocking I/O with ASIO: Complete Implementation Guide

> Learn to perform non-blocking I/O with ASIO by initiating async operations. This guide shows you how to leverage ASIO's event loop for efficient, non-blocking communication.

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

---

**You perform non-blocking I/O with ASIO by initiating `async_read` or `async_write` operations on stream objects, which return immediately and invoke a completion handler later through the `io_context` event loop.**

ASIO (Asynchronous I/O) is a cross-platform C++ library for network and low-level I/O programming. To perform non-blocking I/O with ASIO, you leverage the `io_context` event loop and asynchronous initiating functions defined in headers like [`asio/read.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/read.hpp) and [`asio/write.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/write.hpp). This guide examines the actual implementation in the `chriskohlhoff/asio` repository, showing how these components work together to enable scalable asynchronous operations.

## Core Concepts for Non-Blocking I/O

ASIO’s non-blocking architecture relies on several key abstractions that separate the initiation of an operation from its completion.

### The io_context Event Loop

The **io_context** class, defined in [`asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/io_context.hpp), serves as the central event loop that dispatches I/O completion events. When you call an asynchronous function, the operation is posted to the `io_context`’s executor, which returns control to your thread immediately. The underlying OS operation continues in the background until readiness is signaled, at which point the library invokes your completion handler.

### AsyncReadStream and AsyncWriteStream Concepts

ASIO uses concept-based programming to define stream requirements. The **AsyncReadStream** and **AsyncWriteStream** concepts specify that a type must satisfy certain interface requirements to be used with generic async I/O functions. These concepts are implemented by classes like `basic_socket` in [`asio/basic_socket.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/basic_socket.hpp), which provides the primitive `async_read_some` and `async_write_some` operations that higher-level functions consume.

### Completion Tokens and Handlers

The **CompletionToken** mechanism determines how asynchronous results are delivered to your code. According to [`asio/default_completion_token.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/default_completion_token.hpp), you can choose from:
- **Callback functions** (functors or lambdas) that receive `std::error_code` and bytes transferred
- **`std::future`** via `asio::use_future` (defined in [`asio/use_future.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_future.hpp))
- **C++20 coroutines** via `asio::use_awaitable` (defined in [`asio/use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_awaitable.hpp))

When you call `async_read` (starting at line 45 in [`asio/read.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/read.hpp)) or `async_write` (starting at line 45 in [`asio/write.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/write.hpp)), the function returns immediately and the completion handler runs later on the specified executor.

## Typical Non-Blocking Workflow

To implement non-blocking I/O, follow this four-step pattern:

1. **Create an `io_context`** instance to manage the event loop.
2. **Open a stream object** (such as `tcp::socket`) and establish connectivity.
3. **Initiate asynchronous operations** using `async_read`, `async_write`, or `async_connect` with your chosen completion token.
4. **Run the event loop** by calling `io_context.run()` on one or more threads to process completions.

## Implementation Approaches

### Callback-Based Asynchronous Operations

The traditional approach uses functors or lambdas as completion handlers. This provides maximum flexibility for immediate error handling and chaining operations.

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

using asio::ip::tcp;

void example_callback()
{
    asio::io_context ctx;
    tcp::socket sock(ctx);
    // Assume sock is already connected...

    std::array<char, 1024> data;

    // Asynchronously read exactly 512 bytes
    asio::async_read(sock,
        asio::buffer(data),
        asio::transfer_exactly(512),
        [&](std::error_code ec, std::size_t bytes_transferred)
        {
            if (!ec)
            {
                std::cout << "Read " << bytes_transferred << " bytes\n";
            }
            else
            {
                std::cerr << "Read error: " << ec.message() << "\n";
            }
        });

    // Asynchronously write a string
    std::string msg = "Hello, ASIO!\n";
    asio::async_write(sock,
        asio::buffer(msg),
        [&](std::error_code ec, std::size_t /*unused*/)
        {
            if (ec) std::cerr << "Write error: " << ec.message() << "\n";
        });

    ctx.run();      // Blocks until all work is complete
}

```

The lambda handlers capture the result parameters (`error_code` and `bytes_transferred`) defined in the `async_read` and `async_write` signatures in [`asio/read.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/read.hpp) and [`asio/write.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/write.hpp).

### Future-Based Asynchronous I/O

For code that requires blocking-style semantics at specific synchronization points, use `asio::use_future` to convert async operations into `std::future` objects.

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

using asio::ip::tcp;

void example_future()
{
    asio::io_context ctx;
    tcp::socket sock(ctx);
    // Connect socket...

    std::string msg = "Future style\n";
    auto write_fut = asio::async_write(sock,
                     asio::buffer(msg),
                     asio::use_future);   // Returns std::future<std::size_t>

    // Perform other work here...

    write_fut.get();                     // Blocks until the write finishes
    ctx.run();                           // Process any pending completions
}

```

This pattern, defined in [`asio/use_future.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_future.hpp), allows integration with existing thread-based code while maintaining non-blocking internals.

### C++20 Coroutine Style (co_await)

Modern ASIO supports C++20 coroutines through the `awaitable` class template in [`asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/awaitable.hpp), enabling sequential-looking code that executes asynchronously.

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

using asio::ip::tcp;
using asio::awaitable;
using asio::use_awaitable;

awaitable<void> echo(tcp::socket socket)
{
    std::array<char, 1024> buf;
    for (;;)
    {
        std::size_t n = co_await socket.async_read_some(
                            asio::buffer(buf), use_awaitable);
        if (n == 0) co_return;          // Connection closed
        co_await asio::async_write(socket,
                                   asio::buffer(buf, n),
                                   use_awaitable);
    }
}

int main()
{
    asio::io_context ctx;
    tcp::acceptor acc(ctx, tcp::endpoint(tcp::v4(), 12345));

    // Launch a coroutine for each incoming connection
    asio::co_spawn(ctx,
        [&]()->awaitable<void>
        {
            for (;;)
            {
                tcp::socket sock = co_await acc.async_accept(use_awaitable);
                asio::co_spawn(ctx, echo(std::move(sock)), asio::detached);
            }
        },
        asio::detached);

    ctx.run();
}

```

The `co_spawn` helper function, implemented in [`asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/co_spawn.hpp), launches coroutines on the specified executor, while `use_awaitable` (from [`asio/use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_awaitable.hpp)) converts the completion token into an awaitable object that works with `co_await`.

### Serialized Execution with Strands

When multiple threads call `io_context.run()`, handlers for the same socket can execute concurrently, causing data races. The **strand** class in [`asio/strand.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/strand.hpp) provides serialized execution guarantees.

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

using asio::ip::tcp;

void example_strand()
{
    asio::io_context ctx;
    asio::strand<asio::io_context::executor_type> strand(ctx.get_executor());

    tcp::socket sock(ctx);
    // Connect socket...

    // Both write and read are posted through the same strand,
    // guaranteeing they never execute concurrently
    asio::co_spawn(strand,
        [&]() -> asio::awaitable<void>
        {
            std::array<char, 512> buf;
            std::size_t n = co_await sock.async_read_some(
                              asio::buffer(buf), asio::use_awaitable);
            co_await asio::async_write(sock,
                      asio::buffer(buf, n), asio::use_awaitable);
        },
        asio::detached);
    ctx.run();
}

```

The `strand` wrapper ensures that handlers posted through it execute sequentially, even when the `io_context` runs on multiple threads.

## Summary

- **Non-blocking I/O** in ASIO uses the `io_context` event loop to dispatch completion handlers without blocking calling threads.
- **Initiating functions** like `async_read` and `async_write` (defined in [`asio/read.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/read.hpp) and [`asio/write.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/write.hpp)) return immediately and complete later via the `io_context::run()` loop.
- **Completion tokens** determine result delivery: callbacks for flexibility, `asio::use_future` for blocking synchronization, or `asio::use_awaitable` for C++20 coroutines.
- **Thread safety** requires `asio::strand` (from [`asio/strand.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/strand.hpp)) when multiple threads process the same socket to prevent concurrent handler execution.
- **Key files** include [`asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/io_context.hpp) for the event loop, [`asio/basic_socket.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/basic_socket.hpp) for stream concepts, and [`asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/co_spawn.hpp) for coroutine management.

## Frequently Asked Questions

### What is the difference between `async_read` and `async_read_some`?

`async_read` is a composed operation that repeatedly calls `async_read_some` until the buffer is full or a specific condition is met, while `async_read_some` is a primitive operation that reads whatever data is currently available. According to [`asio/read.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/read.hpp), `async_read` handles the logic of calling the underlying stream's `async_read_some` multiple times until completion, whereas `async_read_some` transfers immediately available bytes and completes right away.

### How do I cancel an in-flight asynchronous operation?

ASIO supports operation cancellation through the `cancellation_signal` class defined in [`asio/cancellation_signal.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/cancellation_signal.hpp). You can attach a `cancellation_signal` to an operation and emit a cancellation signal (terminal or partial) to stop the operation. The underlying stream must support cancellation for this to take effect, and the completion handler will typically receive an `operation_aborted` error code.

### Can I use ASIO non-blocking I/O without callbacks?

Yes. You can use `asio::use_future` to receive results as `std::future` objects, or use C++20 coroutines with `asio::use_awaitable` to write sequential-looking code that uses `co_await`. The [`asio/use_future.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_future.hpp) and [`asio/use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/use_awaitable.hpp) headers provide these completion tokens, allowing you to avoid manual callback registration while maintaining non-blocking execution.

### Do I need a strand if I only use one thread?

No. If you call `io_context.run()` on only one thread, handlers execute sequentially by definition, and you do not need an `asio::strand`. However, if you use `io_context::poll()` or run the `io_context` on a thread pool, you must use strands (from [`asio/strand.hpp`](https://github.com/chriskohlhoff/asio/blob/main/asio/strand.hpp)) to serialize access to shared socket state and prevent data races between concurrent handlers.