🟨 Async I/O (files, sockets, timers, processes, fs events, threads, signals)
SaneCppAsync.h is a multi-platform / event-driven asynchronous I/O library.
AsyncEventLoop with co_await syntax while preserving the same request lifetime expectations. It currently covers a small but useful subset of timers, sockets, loop wake-ups, files, file readiness, selected filesystem operations, process exit, signals, background work, child tasks, task groups, cancellation, and timeouts.Await keeps the same platform caveats as Async: POSIX file readiness is exposed directly, while Windows file readiness for normal file or pipe handles currently fails fast at the Await layer instead of pretending to be portable. File and filesystem operations that need blocking work use caller-provided ThreadPool storage.FileSystemWatcher should stay callback-style or use an explicit caller-owned adapter with bounded event storage. Await intentionally does not model watcher streams as direct AwaitEventLoop methods.This is the list of supported async operations:
| Async Operation | Description |
|---|---|
| AsyncSocketConnect | Starts a socket connect operation, connecting to a remote endpoint. |
| AsyncSocketAccept | Starts a socket accept operation, obtaining a new socket from a listening socket. |
| AsyncSocketSend | Starts a socket send operation, sending bytes to a remote endpoint. |
| AsyncSocketReceive | Starts a socket receive operation, receiving bytes from a remote endpoint. |
| AsyncSocketSendTo | Starts an unconnected socket send to operation, sending bytes to a remote endpoint. |
| AsyncSocketReceiveFrom | Starts an unconnected socket receive from operation, receiving bytes from a remote endpoint. |
| AsyncFileRead | Starts a file read operation, reading bytes from a file (or pipe). |
| AsyncFileWrite | Starts a file write operation, writing bytes to a file (or pipe). |
| AsyncLoopTimeout | Starts a Timeout that is invoked only once after expiration (relative) time has passed. |
| AsyncLoopWakeUp | Starts a wake-up operation, allowing threads to execute callbacks on loop thread. |
| AsyncLoopWork | Executes work in a thread pool and then invokes a callback on the event loop thread. |
| AsyncProcessExit | Starts monitoring a process, notifying about its termination. |
| AsyncFileReadiness | Starts a file descriptor readiness operation. |
| AsyncExternalCompletion | Integrates externally-submitted completion based operations with AsyncEventLoop. |
| AsyncSequence | Execute AsyncRequests serially, by submitting the next one after the previous one is completed. |
| AsyncFileSystemOperation | Starts an asynchronous file system operation (open, close, read, write, sendFile, stat, lstat, fstat, etc.) Some operations need a file path and others need a file descriptor. |
| AsyncSignal | Starts monitoring a signal, notifying about its reception. |
It exposes async programming model for common IO operations like reading / writing to / from a file or tcp socket.
Synchronous I/O operations could block the current thread of execution for an undefined amount of time, making it difficult to scale an application to a large number of concurrent operations, or to coexist with other even loop, like for example a GUI event loop. Such async programming model uses a common pattern, where the call fills an AsyncRequest with the required data. The AsyncRequest is added to an AsyncEventLoop that will queue the request to some low level OS IO queue. The event loop can then monitor all the requests in a single call to SC::AsyncEventLoop::run, SC::AsyncEventLoop::runOnce or SC::AsyncEventLoop::runNoWait. These three different run methods cover different integration use cases of the event loop inside of an applications.
The kernel Async API used on each operating systems are the following:
IOCP on Windowskqueue on macOSio_uring on Linuxepoll on Linuxio_uring setup is not available on the system, the library will fallback to epoll.If an operation may require blocking work on a backend, the caller must provide a SC::ThreadPool through the operation-specific API. Backends that can perform the operation natively, such as Linux io_uring file I/O, prefer native async by default. Pass SC::AsyncThreadPoolMode::ForceThreadPool to force using the supplied pool.
🟨 MVP
This is usable but needs some more testing and a few more features.
This is the list of videos that have been recorded showing some of the internal thoughts that have been going into this library:
Some relevant blog posts are:
An async operation is struct derived from AsyncRequest asking for some I/O to be done made to the OS.
Every async operation has an associated callback that is invoked when the request is fulfilled. If the start function returns a valid (non error) Return code, then the user callback will be called both in case of success and in case of any error.
If the function returns an invalid Return code or if the operation is manually cancelled with SC::AsyncRequest::stop, then the user callback will not be called.
result.reactivateRequest(true) is NOT called) then the async request can be freed as soon as the user callback is called (even inside the callback itself).result.reactivateRequest(true) is called) then the async cannot be freed as it's still in use.Some implementation details: SC::AsyncRequest::state dictates the lifetime of the async request according to a state machine.
Regular Lifetime of an Async request (called just async in the paragraph below):
Cancellation of an async: An async can be cancelled at any time:
Any other case is considered an error (trying to cancel an async already being cancelled or being teardown).
Asynchronous I/O (files, sockets, timers, processes, fs events, threads wake-up) (see Async) AsyncEventLoop pushes all AsyncRequest derived classes to I/O queues in the OS.
Basic lifetime for an event loop is:
Event loop can be run in different ways to allow integrated it in multiple ways in applications.
| Run mode | Description |
|---|---|
| SC::AsyncEventLoop::run | Blocks until there are no more counted active, submitted, or cancelling requests, dispatching completions. It's useful for applications where the eventLoop is the only (or the main) loop. One example could be a console based app doing socket IO or a web server. Waiting on kernel events blocks the current thread with 0% CPU utilization. AsyncEventLoop::interrupt makes this function return before active work has drained. Requests excluded with AsyncEventLoop::excludeFromActiveCount do not keep this function alive.
|
| SC::AsyncEventLoop::runOnce | Blocks until at least one request proceeds, ensuring forward progress, dispatching ready completions. It's useful for application where it's needed to run some idle work after every IO event. Waiting on requests blocks the current thread with 0% CPU utilization. This function is a shortcut invoking async event loop building blocks:
|
| SC::AsyncEventLoop::runNoWait | Process ready requests if any, dispatching their completions, or returns immediately without blocking. It's useful for game-like applications where the event loop runs every frame and one would like to check and dispatch its I/O callbacks in-between frames. This call allows poll-checking I/O without blocking.
|
Alternatively user can explicitly use three methods to submit, poll and dispatch events. This is very useful to integrate the event loop into applications with other event loops (for example GUI applications).
| Run mode | Description | ||
|---|---|---|---|
| SC::AsyncEventLoop::submitRequests | Submits all queued async requests without running user callbacks. An AsyncRequest becomes queued after user calls its specific AsyncRequest::start method.
| ||
| SC::AsyncEventLoop::blockingPoll | Blocks until at least one event happens, ensuring forward progress, without executing completions. It's one of the three building blocks of AsyncEventLoop::runOnce allowing co-operation of AsyncEventLoop within another event loop (for example a GUI event loop or another IO event loop). User callbacks are not invoked by this function; they run when AsyncEventLoop::dispatchCompletions is called. AsyncEventLoopListeners callbacks, if installed, are invoked around this blocking poll. One possible example of such integration with a GUI event loop could:
Waiting on requests blocks the current thread with 0% CPU utilization.
| ||
| SC::AsyncEventLoop::dispatchCompletions | Invokes completions for the AsyncKernelEvents collected by a call to AsyncEventLoop::blockingPoll. This is typically done when user wants to poll for events on a thread (calling AsyncEventLoop::blockingPoll) and dispatch the callbacks on another thread (calling AsyncEventLoop::dispatchCompletions). User callbacks run on the thread calling this function. The typical example would be integrating AsyncEventLoop with a GUI event loop.
|
Monitors Async I/O events from a background thread using a blocking kernel function (no CPU usage on idle).
AsyncEventLoopMonitor makes it easy to integrate AsyncEventLoop within a GUI event loop or another I/O event loop. This pattern avoids constantly polling the kernel, using virtually 0% of CPU time when waiting for events.
| Functions | Description |
|---|---|
| SC::AsyncEventLoopMonitor::startMonitoring | Queue all async requests submissions and start monitoring loop events on a background thread. On the background thread AsyncEventLoop::blockingPoll will block (with 0% CPU usage) and return only when it will be informed by the kernel of some new events. Immediately after AsyncEventLoopMonitor::onNewEventsAvailable will be called (on the background thread). In the code handler associated with this event, the user/caller should inform its main thread to call AsyncEventLoopMonitor::stopMonitoringAndDispatchCompletions. |
| SC::AsyncEventLoopMonitor::stopMonitoringAndDispatchCompletions | Stops monitoring events on the background thread and dispatches callbacks for completed requests. This is typically called by the user of this class on the main thread or in general on the thread where the event loop that coordinates the application lives (GUI thread typically or another I/O Event Loop thread).
|
Starts a Timeout that is invoked only once after expiration (relative) time has passed.
Starts a wake-up operation, allowing threads to execute callbacks on loop thread.
SC::AsyncLoopWakeUp::callback will be invoked on the thread running SC::AsyncEventLoop::run (or its variations) after SC::AsyncLoopWakeUp::wakeUp has been called.
An EventObject can be wait-ed to synchronize further actions from the thread invoking the wake up request, ensuring that the callback has finished its execution.
Executes work in a thread pool and then invokes a callback on the event loop thread.
AsyncLoopWork::work is invoked on one of the thread supplied by the ThreadPool passed during AsyncLoopWork::start. AsyncLoopWork::callback will be called as a completion, on the event loop thread AFTER work callback is finished.
Starts monitoring a process, notifying about its termination.
Process library can be used to start a process and obtain the native process handle.
Starts monitoring a signal, notifying about its reception.
AsyncSignal follows the base AsyncRequest lifecycle: after a delivered signal, the request is completed unless the callback calls Result::reactivateRequest(true). AsyncSignalOptions::mode and coalesce are kept as explicit policy fields for future backend-parity work, but portable code should not rely on them for automatic reactivation.
On POSIX systems, signals like SIGINT, SIGTERM, SIGHUP are supported. On Windows, console control signals CTRL_C_EVENT, CTRL_BREAK_EVENT, and CTRL_CLOSE_EVENT are mapped to SIGINT (2), signal 21, and SIGTERM (15) respectively.
Signal watcher capabilities:
Starts a socket accept operation, obtaining a new socket from a listening socket.
The callback is called with a new socket connected to the given listening endpoint will be returned.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket.
Alternatively SC::AsyncEventLoop::createAsyncTCPSocket creates and associates the socket to the loop.
Starts a socket connect operation, connecting to a remote endpoint.
Callback will be called when the given socket is connected to ipAddress.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket.
Alternatively SC::AsyncEventLoop::createAsyncTCPSocket creates and associates the socket to the loop.
Starts a socket send operation, sending bytes to a remote endpoint.
Callback will be called when the given socket is ready to send more data.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket or though AsyncSocketAccept.
Alternatively SC::AsyncEventLoop::createAsyncTCPSocket creates and associates the socket to the loop.
Starts a socket receive operation, receiving bytes from a remote endpoint.
Callback will be called when some data is read from socket.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket or though AsyncSocketAccept.
Alternatively SC::AsyncEventLoop::createAsyncTCPSocket creates and associates the socket to the loop.
Additional notes:
Starts an unconnected socket send to operation, sending bytes to a remote endpoint.
Callback will be called when the given socket is ready to send more data.
Typical use case is to send data to an unconnected UDP socket.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket or though AsyncSocketAccept.
Alternatively SC::AsyncEventLoop::createAsyncUDPSocket creates and associates the socket to the loop.
Starts an unconnected socket receive from operation, receiving bytes from a remote endpoint.
Callback will be called when some data is read from socket.
Typical use case is to receive data from an unconnected UDP socket.
Socket library can be used to create a Socket but the socket should be created with SC::SocketFlags::NonBlocking and associated to the event loop with SC::AsyncEventLoop::associateExternallyCreatedSocket or though AsyncSocketAccept.
Alternatively SC::AsyncEventLoop::createAsyncUDPSocket creates and associates the socket to the loop.
Starts a file read operation, reading bytes from a file (or pipe).
Callback will be called when the data read from the file (or pipe) is available.
Call AsyncRequest::executeOn to set a thread pool if this is a buffered file and not a pipe. This is important on APIs with blocking behaviour on buffered file I/O (all APIs except io_uring).
File library can be used to open the file and obtain a file (or pipe) descriptor handle.
O_DIRECT or Windows FILE_FLAG_WRITE_THROUGH & FILE_FLAG_NO_BUFFERING should instead avoid using the Task parameter for best performance.When not using the Task remember to:
false)Additional notes:
io_uring backend will not use thread pool by default because that API allows proper async file read/writesAsyncThreadPoolMode::ForceThreadPool to executeOn to force using the supplied thread poolStarts a file write operation, writing bytes to a file (or pipe).
Callback will be called when the file is ready to receive more bytes to write.
Call AsyncRequest::executeOn to set a thread pool if this is a buffered file and not a pipe. This is important on APIs with blocking behaviour on buffered file I/O (all APIs except io_uring).
File library can be used to open the file and obtain a blocking or non-blocking file descriptor handle.
O_DIRECT or Windows FILE_FLAG_WRITE_THROUGH & FILE_FLAG_NO_BUFFERING should instead avoid using the Task parameter for best performance.When not using the Task remember to:
false)Additional notes:
io_uring backend will not use thread pool by default because that API allows proper async file read/writesAsyncThreadPoolMode::ForceThreadPool to executeOn to force using the supplied thread poolWhen using offsets, prefer a single contiguous buffer for portable examples and higher-level helpers. Combining scatter/gather buffers with an explicit offset needs backend-specific validation before being treated as a portable idiom.
Starts a file descriptor readiness operation.
Uses kevent (macOS), epoll (Linux) and io_uring (Linux). Callback will be called when the OS signals readiness events on the given file descriptor.
Integrates externally-submitted completion based operations with AsyncEventLoop.
Manual mode is created with start(eventLoop) and completed with AsyncEventLoop::postExternalCompletion(). On Windows, native mode is created with start(eventLoop, handle) and completed by IOCP using getWindowsOverlapped().
Execute AsyncRequests serially, by submitting the next one after the previous one is completed.
Requests are being queued on a sequence using AsyncRequest::executeOn. AsyncTaskSequence can be used to force running asyncs on a thread (useful for buffered files)
An AsyncSequence using a SC::ThreadPool to execute one or more SC::AsyncRequest in a background thread.
Calling SC::AsyncRequest::executeOn on multiple requests with the same SC::AsyncTaskSequence queues them to be serially executed on the same thread.
Starts an asynchronous file system operation (open, close, read, write, sendFile, stat, lstat, fstat, etc.) Some operations need a file path and others need a file descriptor.
AsyncThreadPoolMode::ForceThreadPool is used. Example of async open operation:
Example of async close operation:
Example of async read operation:
Example of async write operation:
Example of async copy operation:
Example of async copy directory operation:
Example of async rename operation:
Example of async remove empty directory operation:
Example of async remove file operation:
Library abstracts async operations by exposing a completion based mechanism. This mechanism currently maps on kqueue on macOS and OVERLAPPED on Windows.
On Linux it tries to create a direct io_uring backend first and falls back to epoll when the kernel or runtime policy does not allow it. No liburing shared library is required.
The api works on file and socket descriptors, that can be obtained from the File and Socket libraries. It also works with serial descriptors from SerialPort, by using AsyncFileRead and AsyncFileWrite on an opened SC::SerialDescriptor. Pipe endpoints accepted/connected through SC::NamedPipeServer and SC::NamedPipeClient, exposed as SC::PipeDescriptor are also supported.
On Windows, AsyncTest includes an optional real COM section named serial com0com read/write. Set SC_TEST_COM0COM_PORT_A and SC_TEST_COM0COM_PORT_B (values can be COMx or \\.\COMx) to enable it. When variables are unset, this section prints a skip message and succeeds.
The entire library is free of allocations, as it uses a double linked list inside SC::AsyncRequest.
Caller is responsible for keeping AsyncRequest-derived objects memory stable until async callback is called.
SC::ArenaMap from the Containers can be used to preallocate a bounded pool of Async objects.
🟩 Usable Features:
🟦 Complete Features:
LOC counts exclude comments. Library counts files physically under Libraries/Async. Single File counts SaneCppAsync.h. Standalone counts SaneCppAsyncStandalone.h and intentionally includes dependency payloads.
| Metric | Header | Source | Sum |
|---|---|---|---|
| Library | 1926 | 5842 | 7768 |
| Single File | 1646 | 6974 | 8620 |
| Standalone | 6190 | 13279 | 19469 |