Handling fork() with ASIO io_context in Multiprocess Applications

ASIO provides a fork-notification API through asio::execution_context::notify_fork() that must be called before, during, and after fork() to safely reuse io_context objects in child processes without undefined behavior or crashes.

When building multiprocess applications with the chriskohlhoff/asio library, calling fork() without notifying the execution context leads to corrupted internal state, duplicated file descriptors, and potential deadlocks. The library implements a specific notification protocol through the execution_context base class that allows services to reset their state across process boundaries. Understanding this mechanism is essential for reliable handling of fork() with ASIO io_context in multiprocess applications.

Why fork() Breaks ASIO Execution Contexts

ASIO’s architecture relies on service registries, thread pools, and reactor mechanisms that maintain open file descriptors and internal state machines. When a process calls fork(), the child inherits the parent's address space, but OS resources like epoll or kqueue file descriptors become shared or duplicated in undefined ways.

Without explicit notification, the child process inherits invalid internal pointers, duplicated timers, and inconsistent work counters. In include/asio/execution_context.hpp, the library defines the fork_event enumeration to categorize these transitions, ensuring each registered service can execute cleanup or reinitialization logic appropriate to the process lifecycle.

The Fork Notification API

The asio::execution_context class provides the central coordination point for fork safety. All derived contexts—including io_context and thread_pool—inherit this functionality directly.

Fork Event Types

The fork_event enum defined in include/asio/execution_context.hpp (lines 79-92) distinguishes three critical phases:

  • fork_prepare – Signals that the process is about to call fork(). Services should release locks and flush buffers.
  • fork_parent – Executed in the parent process after fork() returns the child's PID. Services restore normal operation.
  • fork_child – Executed in the child process after fork() returns 0. Services must rebuild internal state, close duplicated descriptors, and reset work counters.

Implementation Details

The notify_fork() method in include/asio/execution_context.hpp (lines 98-108) iterates through all registered services and invokes their individual notify_fork() hooks. This centralized dispatch ensures consistent state management across timers, sockets, and custom services.

Additionally, io_context inherits from execution_context (as seen in include/asio/io_context.hpp, lines 78-86), automatically gaining fork-awareness. For executors requiring explicit fork semantics, io_context::basic_executor_type provides a require() overload for relationship.fork (lines 108-118), enabling fine-grained control over work tracking across process boundaries.

Step-by-Step Implementation Pattern

To safely use an io_context across a fork() boundary, follow this strict notification sequence:

  1. Prepare: Call ctx.notify_fork(asio::execution_context::fork_prepare) before invoking fork().
  2. Fork: Execute pid_t pid = ::fork().
  3. Child: If pid == 0, call ctx.notify_fork(asio::execution_context::fork_child) immediately.
  4. Parent: If pid > 0, call ctx.notify_fork(asio::execution_context::fork_parent) before resuming operations.

This protocol ensures that internal service registries in include/asio/execution_context.hpp properly handle file descriptor duplication and work queue resets.

Complete Code Examples

Basic Fork Pattern

The following example demonstrates the core notification sequence required when handling fork() with ASIO io_context:

#include <asio.hpp>
#include <unistd.h>

int main()
{
    asio::io_context ctx;

    // 1. Tell ASIO we are about to fork.
    ctx.notify_fork(asio::execution_context::fork_prepare);

    if (pid_t pid = ::fork())
    {
        // -------------------- Parent process --------------------
        if (pid > 0)
        {
            // 3. Notify the parent side.
            ctx.notify_fork(asio::execution_context::fork_parent);
        }

        // Parent can continue using ctx.
        ctx.run();
    }
    else
    {
        // -------------------- Child process --------------------
        // 3. Notify the child side.
        ctx.notify_fork(asio::execution_context::fork_child);

        // Child may reuse the existing io_context or create a new one.
        ctx.run();
        _exit(0);
    }
}

Forking a Network Server

This pattern extends to network servers that spawn child processes to handle individual connections:

#include <asio.hpp>
#include <iostream>
#include <unistd.h>

void session(asio::ip::tcp::socket sock)
{
    // Echo server logic...
}

void accept_connections(asio::io_context& ctx,
                        asio::ip::tcp::acceptor& acceptor)
{
    acceptor.async_accept([&](std::error_code ec,
                              asio::ip::tcp::socket sock)
    {
        if (!ec)
        {
            // Fork a new process to handle the connection.
            ctx.notify_fork(asio::execution_context::fork_prepare);
            if (pid_t pid = ::fork())
            {
                // Parent – keep accepting.
                if (pid > 0)
                    ctx.notify_fork(asio::execution_context::fork_parent);
                accept_connections(ctx, acceptor);
            }
            else
            {
                // Child – handle the single session.
                ctx.notify_fork(asio::execution_context::fork_child);
                session(std::move(sock));
                _exit(0);
            }
        }
    });
}

int main()
{
    asio::io_context ctx;
    asio::ip::tcp::acceptor acc(ctx,
        asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));
    accept_connections(ctx, acc);
    ctx.run();
}

Using Executors with Fork Relationship

For applications using the executor model, explicitly require the fork relationship to ensure proper work tracking:

auto exec = ctx.get_executor();
auto fork_exec = asio::require(exec, asio::execution::relationship.fork);

// Now fork_exec respects fork semantics for custom services.
asio::post(fork_exec, []{ std::cout << "Running with fork awareness\n"; });

The relationship.fork property is defined in include/asio/execution/relationship.hpp and handled specifically in io_context::basic_executor_type to manage outstanding work counters across process boundaries.

Critical Safety Considerations

Thread Safety: You must call notify_fork() only when no other ASIO functions are active on other threads. Calling from within a completion handler is safe because the handler already executes within the context's thread.

Multiple Contexts: Each io_context or thread_pool instance you intend to use after the fork requires separate notification. The execution_context::notify_fork() implementation iterates through each service registry individually, so omitting a context leaves its services in an invalid state.

Work Counters: Without the fork_child notification, the child process inherits incorrect outstanding work counts from the parent, potentially causing the event loop to terminate immediately. The notification resets these counters in include/asio/execution_context.hpp.

Summary

  • Always notify before forking: Call notify_fork(asio::execution_context::fork_prepare) prior to fork() to allow services to release critical resources.
  • Handle both sides: Invoke fork_child in the child process and fork_parent in the parent to reinitialize internal state and file descriptors.
  • Use fork-aware executors: Apply asio::require(exec, asio::execution::relationship.fork) when executors must track work across process boundaries.
  • Reference the source: The implementation resides in include/asio/execution_context.hpp and include/asio/io_context.hpp, with the fork_event enum defined at lines 79-92.

Frequently Asked Questions

What happens if I don't call notify_fork() after forking?

Without calling notify_fork(), the child process inherits duplicated file descriptors, invalid timer handles, and corrupted service registries from the parent. This leads to undefined behavior including crashes, deadlocks, and lost I/O events as the child attempts to use resources that are only valid in the parent context.

Can I use the same io_context in both parent and child after fork?

Yes, provided you follow the notification protocol. After calling notify_fork(asio::execution_context::fork_child) in the child and notify_fork(asio::execution_context::fork_parent) in the parent, both processes can safely continue using their respective instances of the io_context. The notifications trigger service-specific reinitialization that makes the inherited context valid in each process.

Is notify_fork() thread-safe?

No, notify_fork() is not thread-safe relative to other ASIO operations. You must ensure no other threads are calling ASIO functions when notify_fork() executes. The safest approach is to call it from within a completion handler running on the io_context thread, or to ensure all other threads have completed their ASIO work before forking.

Does this apply to thread_pool as well as io_context?

Yes, the fork notification mechanism applies to any class derived from asio::execution_context, including asio::thread_pool. Each context maintains its own service registry in include/asio/execution_context.hpp, so you must call notify_fork() on every context instance that will remain active after the fork operation.

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 →