🟨 Async I/O (files, sockets, timers, processes, fs events, threads wake-up)
Async is a multi-platform / event-driven asynchronous I/O library.
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 macOSepoll
on Linuxio_uring
on Linux (dynamically loading liburing
)liburing
is not available on the system, the library will transparently fallback to epoll.If an async operation is not supported by the OS, the caller can provide a SC::ThreadPool to run it on a thread. See SC::AsyncFileRead / SC::AsyncFileWrite for an example.
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. |
AsyncSocketClose | Starts a socket close operation. |
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). |
AsyncFileClose | Starts a file close operation, closing the OS file descriptor. |
AsyncLoopTimeout | Starts a Timeout that is invoked 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. |
AsyncFilePoll | Starts an handle polling operation. |
🟨 MVP
This is usable but needs some more testing and a few more features.
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.
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 that may already have an existing event loop (example GUI applications).
Run mode | Description |
---|---|
SC::AsyncEventLoop::run | Blocks until there are no more active queued requests. 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 requests blocks the current thread with 0% CPU utilization. |
SC::AsyncEventLoop::runOnce | Blocks until at least one request proceeds, ensuring forward progress. It's useful for applications where the eventLoop events needs to be interleaved with other work. For example one possible way of integrating with a UI event loop could be to schedule a recurrent timeout timer every 1/60 seconds where calling GUI event loop updates every 60 seconds, blocking for I/O for the remaining time. Waiting on requests blocks the current thread with 0% CPU utilization. |
SC::AsyncEventLoop::runNoWait | Process active requests if they exist 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. |
Starts a Timeout that is invoked 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 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::associateExternallyCreatedTCPSocket.
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::associateExternallyCreatedTCPSocket.
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::associateExternallyCreatedTCPSocket 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::associateExternallyCreatedTCPSocket or though AsyncSocketAccept.
Alternatively SC::AsyncEventLoop::createAsyncTCPSocket creates and associates the socket to the loop.
Starts a socket close operation. Callback will be called when the socket has been fully closed.
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.
Use the start overload with ThreadPool
/ Task
parameters to execute file read on a background thread. This is important on APIs with blocking behaviour on buffered file I/O (all apis with the exception of 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
)Starts 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.
Use the start overload with ThreadPool
/ Task
parameters to execute file read on a background thread. This is important on APIs with blocking behaviour on buffered file I/O (all apis with the exception of 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
)Starts a file close operation, closing the OS file descriptor. Callback will be called when the file is actually closed.
File library can be used to open the file and obtain a blocking or non-blocking file descriptor handle.
Starts an handle polling operation. Uses GetOverlappedResult
(windows), kevent
(macOS), epoll
(Linux) and io_uring
(Linux). Callback will be called when any of the three API signals readiness events on the given file descriptor. Check File System Watcher for an example usage of this notification.
Library abstracts async operations by exposing a completion based mechanism. This mechanism currently maps on kqueue
on macOS and OVERLAPPED
on Windows.
It currently tries to dynamically load io_uring
on Linux doing an epoll
backend fallback in case liburing
is not available on the system. There is not need to link liburing
because the library loads it dynamically and embeds the minimal set of static
inline
functions needed to interface with it.
The api works on file and socket descriptors, that can be obtained from the File and Socket libraries.
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:
💡 Unplanned Features: