What Are ASIO's I/O Objects and How Are They Used: A Complete Guide
ASIO's I/O objects are class templates that wrap asynchronous resources—including sockets, timers, and acceptors—and bind them to an executor that dispatches completion handlers through an event loop.
ASIO (the standalone version of Boost.Asio maintained in the chriskohlhoff/asio repository) models every asynchronous resource as an I/O object. These objects serve as the primary interface between your application code and the underlying operating system resources, providing a consistent API for initiating asynchronous operations and handling completions.
Core Architecture of ASIO I/O Objects
The architecture relies on a clear separation between the user-facing object, the executor that schedules work, and the service that performs the actual I/O.
The basic_io_object Base Class
All I/O objects inherit (directly or indirectly) from basic_io_object, defined in include/asio/basic_io_object.hpp at lines 53-56. Although this base class is deprecated, it remains the foundation for the current implementation, storing the underlying implementation and providing access to the associated io_context.
Executor Association with any_io_executor
Every I/O object has an executor_type typedef, defaulting to any_io_executor. This polymorphic executor, defined in include/asio/any_io_executor.hpp at lines 37-40, satisfies the property set required by any I/O object and allows generic code to work with different executors—whether from a thread pool, io_context, or custom implementations.
As shown in include/asio/basic_socket.hpp at lines 74-76, the executor controls how completion handlers are dispatched. When you construct an I/O object with an io_context, the object captures the context's executor, ensuring that all callbacks run on that executor's event loop.
The io_context as the Core I/O Service
The io_context class, declared in include/asio/io_context.hpp at lines 66-73, owns the low-level services that perform I/O (such as the socket service and timer service). It also supplies the default executor for all I/O objects created from it. The io_context represents the central hub where asynchronous operations are registered and where their completions are processed.
Common Types of I/O Objects in ASIO
ASIO provides specialized I/O objects for different asynchronous resources:
ip::tcp::socket– Stream-oriented TCP socket for reliable, bidirectional communication.ip::udp::socket– Datagram socket for connectionless UDP communication.ip::tcp::acceptor– Listening socket that accepts incoming TCP connections.steady_timeranddeadline_timer– Timer objects for time-based asynchronous events, withdeadline_timerdefined ininclude/asio/basic_deadline_timer.hpp.basic_pipe– Pipe handle for inter-process communication on supported platforms.
Each of these types follows the same pattern: they inherit the base characteristics, expose an executor, and provide asynchronous member functions that initiate operations through the underlying service.
How to Use ASIO I/O Objects
Working with I/O objects follows a consistent lifecycle across all resource types.
1. Construction with an Executor
An I/O object must be constructed with either an explicit executor or an io_context (from which the executor is obtained).
asio::io_context ctx;
asio::ip::tcp::socket sock(ctx); // Uses ctx's executor
// Or with an explicit executor
asio::any_io_executor exec = ctx.get_executor();
asio::steady_timer timer(exec);
2. Opening and Binding
Many objects, particularly sockets, must be opened or bound before use. Constructor overloads in basic_socket.hpp can open the resource automatically, or you can call open() and bind() explicitly after construction.
3. Initiating Asynchronous Operations
The object provides async_* member functions that accept completion tokens or handlers. Internally, these functions forward the request to the underlying service (socket service, timer service, etc.) and register the handler with the associated executor.
sock.async_connect(endpoint,
[&](const asio::error_code& ec) {
if (!ec) std::cout << "connected!\n";
});
4. Running the Event Loop
The io_context (or any executor driving the I/O objects) must be run to process pending asynchronous operations.
ctx.run(); // Blocks until all work is finished
5. Work Tracking for Composed Operations
When composing operations using asio::compose or asio::co_spawn, additional I/O objects can be passed to keep their work alive. This is why many APIs accept a variadic io_objects_or_executors parameter, as referenced in include/asio/compose.hpp at line 41. The executor ensures that the objects remain valid until all asynchronous operations complete.
Practical Code Examples
TCP Echo Server
This example demonstrates the core I/O objects: io_context, ip::tcp::acceptor, and ip::tcp::socket.
#include <asio.hpp>
int main()
{
// 1. Core I/O object – io_context
asio::io_context ctx;
// 2. Listener (I/O object)
asio::ip::tcp::acceptor acceptor(
ctx,
asio::ip::tcp::endpoint(asio::ip::make_address("0.0.0.0"), 12345));
// 3. Accept loop
std::function<void()> accept_loop;
accept_loop = [&]{
auto socket = std::make_shared<asio::ip::tcp::socket>(ctx);
acceptor.async_accept(*socket,
[&, socket](const asio::error_code& ec){
if (!ec) {
// 4. Echo back whatever is read
std::array<char, 1024> data;
socket->async_read_some(asio::buffer(data),
[socket, data](const asio::error_code& ec, std::size_t n) mutable {
if (!ec) {
asio::async_write(*socket,
asio::buffer(data, n), [](auto...){});
}
});
}
accept_loop(); // keep accepting
});
};
accept_loop();
// 5. Run the event loop – drives all I/O objects
ctx.run();
}
Timer with Thread Pool Executor
This example shows how I/O objects can use executors from sources other than io_context, such as thread_pool.
#include <asio.hpp>
#include <iostream>
int main()
{
// 1. Create a thread-pool executor (I/O executor)
asio::thread_pool pool(4);
asio::any_io_executor exec = pool.get_executor();
// 2. Timer I/O object bound to the executor
asio::steady_timer timer(exec, std::chrono::seconds(2));
// 3. Async wait
timer.async_wait([&](const asio::error_code& ec){
if (!ec) std::cout << "Timer fired on thread "
<< std::this_thread::get_id() << "\n";
});
// 4. Run the pool – this drives the timer's executor
pool.join();
}
Composing Multiple I/O Objects
This coroutine example illustrates how multiple I/O objects (socket and timer) can be kept alive within a composed operation.
#include <asio.hpp>
#include <iostream>
asio::awaitable<void> echo_with_timeout(asio::ip::tcp::socket sock,
asio::steady_timer timer)
{
char data[512];
for (;;) {
// Wait for either read completion or timeout
std::size_t n = co_await asio::async_read(sock,
asio::buffer(data),
asio::as_tuple(asio::use_awaitable));
co_await asio::async_write(sock,
asio::buffer(data, n),
asio::use_awaitable);
timer.expires_after(std::chrono::seconds(10));
co_await timer.async_wait(asio::use_awaitable); // keep connection alive
}
}
The coroutine receives two I/O objects by value, ensuring both remain alive for the duration of the composed operation. This pattern demonstrates the flexibility of ASIO's executor model when managing multiple asynchronous resources.
Summary
- ASIO I/O objects wrap asynchronous resources like sockets, timers, and acceptors, providing a unified interface for initiating operations in the
chriskohlhoff/asiolibrary. - All I/O objects inherit from
basic_io_object(defined ininclude/asio/basic_io_object.hpp) and associate with anany_io_executorthat controls handler dispatch. - The
io_contextserves as the default executor and owns the low-level services that perform I/O, as implemented ininclude/asio/io_context.hpp. - Usage requires constructing objects with an executor, initiating asynchronous operations via
async_*methods, and running the executor's event loop to process completions. - Multiple I/O objects can be composed together using coroutines or
asio::compose, with the executor ensuring proper work tracking and object lifetime management.
Frequently Asked Questions
What is the difference between an I/O object and an executor in ASIO?
An I/O object (such as ip::tcp::socket or steady_timer) represents the actual asynchronous resource and provides methods to initiate operations like async_read or async_wait. An executor (such as any_io_executor or the io_context executor) is responsible for scheduling and dispatching the completion handlers associated with those operations. While the I/O object initiates the work, the executor determines where and when the callback executes.
Can I use ASIO I/O objects without an io_context?
Yes, you can construct I/O objects with any valid executor, such as those from asio::thread_pool or custom strand executors. However, the io_context remains the most common choice because it provides the core I/O services that actually perform the asynchronous operations. If you use a different executor, you must still ensure that an io_context or similar service is running somewhere to process the I/O, or use platform-specific native handles that don't require ASIO's service layer.
How does ASIO ensure I/O objects remain valid during asynchronous operations?
ASIO uses work tracking through the executor associated with the I/O object. When an asynchronous operation is initiated, the service increments the work count for the executor. As shown in include/asio/compose.hpp, composition functions can accept multiple I/O objects to keep them alive until all operations complete. Additionally, using std::shared_ptr or capturing I/O objects in completion handlers (as demonstrated in the TCP echo server example) ensures they remain valid until the callbacks finish executing.
Why is basic_io_object deprecated but still used in the codebase?
The basic_io_object class in include/asio/basic_io_object.hpp is marked as deprecated because ASIO is transitioning toward a more executor-centric model where I/O objects directly store their implementation and executor references. However, the class remains the base for current I/O objects to maintain backward compatibility and provide a migration path. New code should be aware of this deprecation but can rely on the current inheritance structure until the library fully removes the deprecated base class in a future major version.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →