Using ASIO random_access_file with Scatter-Gather I/O

ASIO's basic_random_access_file enables high-performance asynchronous file operations by accepting buffer sequences—collections of multiple buffers that are read or written in a single atomic operation at a specified file offset.

The chriskohlhoff/asio repository provides a portable, asynchronous I/O library where scatter-gather operations allow you to transfer data between non-contiguous memory regions and disk positions efficiently. This pattern minimizes system call overhead by packing multiple buffers into a single vectored I/O operation, implemented via platform-specific services like io_uring on Linux and I/O Completion Ports on Windows.

Understanding Scatter-Gather I/O in ASIO

Scatter-gather I/O refers to reading data from a single file offset into multiple memory buffers (scatter), or writing data from multiple buffers to a single file offset (gather). In ASIO, this is achieved through buffer sequences—types that satisfy the ConstBufferSequence or MutableBufferSequence concepts.

The basic_random_access_file class defined in include/asio/basic_random_access_file.hpp exposes member functions that accept these sequences, allowing the underlying operating system to transfer data directly into your scattered memory layout without requiring intermediate copies.

Buffer Sequence Requirements

Any scatter-gather operation requires a buffer sequence that meets ASIO's concept requirements. Valid types include:

  • std::array<asio::mutable_buffer, N> for fixed-size scatter operations
  • std::vector<asio::const_buffer> for dynamic gather operations
  • Custom types implementing begin() and end() returning buffer iterators

Each buffer in the sequence encapsulates a pointer and size, which the platform-specific service implementations translate into native vectored I/O structures like iovec (POSIX) or WSABUF (Windows).

Core Asynchronous Operations

ASIO provides two layers of abstraction for scatter-gather file I/O: low-level member functions that perform single-shot transfers, and high-level free functions that guarantee complete buffer filling.

Low-Level Member Functions

The basic_random_access_file class provides async_read_some_at and async_write_some_at as defined in include/asio/basic_random_access_file.hpp. These methods initiate a single asynchronous operation that translates directly to a vectored system call:

template<typename MutableBufferSequence, typename ReadToken>
auto async_read_some_at(uint64_t offset, 
                       const MutableBufferSequence& buffers, 
                       ReadToken&& token);

template<typename ConstBufferSequence, typename WriteToken>
auto async_write_some_at(uint64_t offset,
                        const ConstBufferSequence& buffers,
                        WriteToken&& token);

Important: These functions may transfer fewer bytes than the total size of the buffer sequence. The completion handler receives a std::size_t indicating the actual bytes transferred, which can be less than the sum of all buffer sizes if the operation encounters end-of-file or resource limits.

High-Level Free Functions

For applications requiring guaranteed complete transfers, include/asio/read_at.hpp provides asio::async_read_at and asio::async_write_at. These free functions repeatedly invoke the device's async_read_some_at or async_write_some_at until all buffers are completely filled or an error occurs:

asio::async_read_at(file, offset, buffers, handler);
asio::async_write_at(file, offset, buffers, handler);

These helpers preserve ASIO's cancellation support, accepting cancellation_type::terminal, partial, or total to control operation lifecycle.

Platform-Specific Implementations

Under the hood, ASIO dispatches scatter-gather requests to platform-specific service implementations that optimize the buffer sequence translation:

Both services implement the async_read_some_at and async_write_some_at interfaces, ensuring consistent behavior across platforms while leveraging native performance characteristics.

Practical Code Examples

Writing Multiple Buffers Atomically

This example demonstrates gathering data from three separate buffers into a file at offset 0:

asio::io_context ctx;
asio::random_access_file file(ctx, "example.bin",
    asio::random_access_file::write_only |
    asio::random_access_file::create);

// Prepare three buffers.
std::array<asio::const_buffer, 3> bufs = {
    asio::buffer(data1, size1),
    asio::buffer(data2, size2),
    asio::buffer(data3, size3)};

file.async_write_some_at(0, bufs,
    [&](asio::error_code ec, std::size_t bytes_written) {
        if (!ec) {
            std::cout << "Wrote " << bytes_written << " bytes.\n";
        } else {
            std::cerr << "Write error: " << ec.message() << "\n";
        }
    });

ctx.run();

Reading into Scattered Buffers with Coroutines

Using C++20 coroutines, you can scatter-read into multiple buffers with clean asynchronous syntax:

asio::awaitable<void> read_three_buffers() {
    asio::random_access_file file(co_await asio::this_coro::executor,
                                 "example.bin",
                                 asio::random_access_file::read_only);

    std::array<char, 64> b1, b2, b3;
    std::array<asio::mutable_buffer, 3> bufs = {
        asio::buffer(b1),
        asio::buffer(b2),
        asio::buffer(b3)};

    std::size_t n = co_await file.async_read_some_at(0, bufs,
                      asio::use_awaitable);
    std::cout << "Read " << n << " bytes across three buffers.\n";
}

Ensuring Complete Transfers

When your application logic requires all buffers to be filled completely, use the high-level helper instead of the low-level member function:

asio::async_read_at(file, 0, bufs,
    [&](asio::error_code ec, std::size_t total) {
        if (!ec) {
            std::cout << "All " << total << " bytes read.\n";
        }
    });

Key Considerations for Production Use

When implementing scatter-gather I/O with random_access_file, consider these critical aspects:

  • Cancellation: All scatter-gather operations support per-operation cancellation. Use asio::cancel_after or explicit cancellation signals to abort long-running transfers
  • Partial Operations: Always check the bytes_transferred parameter in your completion handler when using async_read_some_at, as short reads are normal for these low-level operations
  • Thread-Safety: random_access_file objects are not copyable; use move semantics to transfer ownership between threads. The underlying service implementations ensure thread-safe completion handler dispatch
  • Offset Management: All operations accept explicit 64-bit offsets, enabling true random access without maintaining file position state

Summary

  • Scatter-gather I/O in ASIO uses buffer sequences to transfer data between multiple memory regions and file offsets in a single operation
  • Low-level functions async_read_some_at and async_write_some_at in basic_random_access_file provide direct access to vectored system calls
  • High-level helpers asio::async_read_at and asio::async_write_at ensure complete buffer filling through automatic retry loops
  • Platform implementations in win_iocp_file_service.hpp and io_uring_file_service.hpp optimize the translation to native I/O vectors
  • Buffer sequences must satisfy MutableBufferSequence or ConstBufferSequence concepts, typically using std::array or std::vector of ASIO buffers

Frequently Asked Questions

What is the difference between async_read_some_at and async_read_at?

async_read_some_at is a member function of basic_random_access_file that performs a single vectored read operation which may return fewer bytes than requested. async_read_at is a free function in read_at.hpp that automatically loops until all buffers are completely filled or an error occurs, providing higher-level convenience at the cost of additional internal bookkeeping.

What buffer types can I use with scatter-gather I/O?

ASIO accepts any type satisfying the ConstBufferSequence or MutableBufferSequence concepts. Common choices include std::array<asio::mutable_buffer, N> for fixed-size operations and std::vector<asio::const_buffer> for dynamic sizes. Custom types are supported if they provide begin() and end() methods returning valid buffer iterators.

How does ASIO handle scatter-gather on Windows vs Linux?

On Windows, win_iocp_file_service.hpp translates buffer sequences into WSABUF arrays for I/O Completion Port operations. On Linux, io_uring_file_service.hpp maps sequences directly to io_uring submission queue entries supporting vectored I/O. Both implementations perform the vectored system call atomically at the requested offset, providing equivalent semantics with platform-optimized performance.

Is random_access_file thread-safe?

The random_access_file object itself is not thread-safe for concurrent operations; you must synchronize access or use strand executors. However, the object is move-only (non-copyable), allowing you to transfer ownership between threads. The underlying service implementations in detail/win_iocp_file_service.hpp and detail/io_uring_file_service.hpp ensure that completion handlers are thread-safe when using appropriate executor configurations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →