Handling SIGINT and SIGTERM Gracefully with ASIO signal_set

Use asio::signal_set to asynchronously monitor POSIX signals like SIGINT and SIGTERM, then trigger a clean shutdown by stopping the io_context from within the signal handler.

The asio::signal_set class in the chriskohlhoff/asio repository provides a portable, asynchronous mechanism for handling process termination signals. Instead of blocking signal handlers or complex signal masking, you can register signals like SIGINT (Ctrl-C) and SIGTERM to gracefully shut down your application by stopping the io_context when these signals arrive.

Core Architecture of signal_set

basic_signal_set Implementation

The asio::signal_set name is actually a typedef for asio::basic_signal_set<>, defined in include/asio/signal_set.hpp. The implementation resides in include/asio/basic_signal_set.hpp, where it inherits from signal_set_base and uses an internal signal_set_service to interact with the operating system kernel.

Signal Registration and Management

You can register signals either through constructor overloads accepting up to three signals simultaneously, or dynamically using the add() and remove() methods. The constructor internally calls add() for each supplied signal. Both add() and remove() provide error-throwing and error-code overloads (e.g., add(int, error_code&)), forwarding to the service which registers the signal with the kernel while safely ignoring duplicate registrations.

Queuing and Delivery Mechanism

If a signal arrives when no handler is waiting, the notification is queued and delivered on the next async_wait. Queued notifications are ordered by ascending signal number, ensuring deterministic delivery. The same signal can be registered on different signal_set objects, with each receiving a handler call—provided the signals were registered through ASIO rather than raw signal() or sigaction() system calls.

Signal Masking and Thread Safety

On POSIX platforms, registered signals must be unblocked in at least one thread for the operating system to deliver them. This requirement is documented in the class description within basic_signal_set.hpp. The signal set uses the same executor as the surrounding io_context, ensuring thread-safe dispatch of signal handlers to the correct execution context.

Implementing Graceful Shutdown

Minimal Example

The pattern from src/examples/cpp11/http/server4/main.cpp (lines 38-49) demonstrates the canonical approach:

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

int main()
{
    asio::io_context io;
    // Create signal_set bound to the io_context executor
    asio::signal_set signals(io);
    
    // Register termination signals
    signals.add(SIGINT);
    signals.add(SIGTERM);
#if defined(SIGQUIT)
    signals.add(SIGQUIT);
#endif

    // Async wait stops the io_context upon signal receipt
    signals.async_wait(
        [&io](const asio::error_code&, int)
        {
            io.stop();  // Graceful shutdown
        });

    io.run();
}

Blocking with Futures

For synchronous-style handling, use asio::use_future:

#include <asio.hpp>

int main()
{
    asio::io_context io;
    asio::signal_set signals(io, SIGINT, SIGTERM);

    // Block until signal arrives
    std::future<int> f = signals.async_wait(asio::use_future);
    int signum = f.get();  // Returns the signal number
    io.stop();
}

Coroutine Integration (C++20)

For modern asynchronous code, use asio::use_awaitable as demonstrated in src/examples/cpp20/coroutines/echo_server.cpp:

#include <asio.hpp>
#include <asio/awaitable.hpp>
#include <asio/use_awaitable.hpp>

asio::awaitable<void> monitor_signals(asio::io_context& ctx)
{
    asio::signal_set sigs(ctx.get_executor(), SIGINT, SIGTERM);
    co_await sigs.async_wait(asio::use_awaitable);  // Suspends until signal
    ctx.stop();                                     // Graceful termination
}

Cancellation and Cleanup

The cancel() method forces pending async_wait operations to complete immediately with asio::error::operation_aborted. This does not modify the set of registered signals, allowing you to abort current waiters without unregistering the signals themselves.

Summary

  • asio::signal_set (typedef for basic_signal_set<>) provides asynchronous POSIX signal handling through include/asio/basic_signal_set.hpp
  • Register signals via constructor overloads or the add() method; remove with remove()
  • Signals are queued and delivered in ascending numerical order if handlers are not immediately available
  • Use async_wait() with lambdas, futures, or coroutines to handle signals without blocking threads
  • Call cancel() to abort pending operations without unregistering signals
  • Ensure signals are unblocked in at least one thread on POSIX systems for delivery to occur
  • Reference implementation available in src/examples/cpp11/http/server4/main.cpp

Frequently Asked Questions

Can I register the same signal on multiple signal_set instances?

Yes. According to the chriskohlhoff/asio source code, the same signal can be registered on different signal_set objects, and each will receive a handler call. This works only for signals registered through ASIO, not through raw signal() or sigaction() system calls.

What happens if a signal arrives before async_wait is called?

The notification is queued and delivered on the next async_wait invocation. Queued notifications are ordered by ascending signal number, ensuring deterministic delivery order when multiple signals are pending.

Do I need to worry about signal masking when using signal_set?

Yes. On POSIX platforms, registered signals must be unblocked in at least one thread for the operating system to deliver them. The ASIO documentation in basic_signal_set.hpp explicitly notes this requirement, though the library handles the internal registration once this prerequisite is met.

How do I stop waiting for signals without destroying the signal_set?

Call cancel() on the signal_set instance. This forces any pending async_wait operations to complete immediately with asio::error::operation_aborted, while keeping the signal registrations active for future wait operations.

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 →