Afon is a bounded structured task runtime for C++20. It runs ordinary callables on a runtime-sized worker pool while keeping admission, cancellation, child-task lifetimes, result propagation, and shutdown behavior explicit.
Afon (pronounced "AV-on") is Welsh for "river." The name carries Rio's flow
metaphor forward while distinguishing this bounded, structured implementation.
Afon is intended for CPU work and controlled use of blocking operations. It is not a socket reactor or coroutine I/O framework.
- Thread-safe submission from multiple producers
- Bounded queue with blocking or rejecting admission
- Move-only callables and
std::futureresults - Cooperative cancellation through
std::stop_token - Structured scopes that join children and cancel siblings after failure
- Ordered
parallel_mapand joinedparallel_for_each - Bounded bulk-operation state for large input ranges
- Current and high-water queue and worker statistics
- Drain and cancel-pending shutdown modes
- No third-party runtime dependency
#include <afon/runtime.hpp>
afon::runtime runtime({
.workers = 4,
.queue_capacity = 256,
});
auto answer = runtime.submit([] {
return 6 * 7;
});
std::cout << answer.get() << '\n';
runtime.shutdown(afon::shutdown_mode::drain);A callable may accept a std::stop_token:
auto result = runtime.submit([](std::stop_token stop) {
while (!stop.stop_requested()) {
perform_chunk();
}
});Cancellation is cooperative. A running callable that ignores its token cannot be forcibly stopped.
runtime::run creates a scope that cannot finish while child tasks remain:
auto page = runtime.run([](afon::scope& work) {
auto profile = work.spawn(load_profile);
auto orders = work.spawn(load_orders);
return render(profile.get(), orders.get());
});If a child fails, the scope requests cancellation for its siblings, waits for all children to settle, and rethrows the first child error.
Related work can share an absolute steady-clock deadline:
auto result = runtime.run_until(deadline, [](afon::scope& work) {
auto first = work.spawn(load_first);
auto second = work.spawn(load_second);
return combine(first.get(), second.get());
});When the deadline expires, the scope requests cooperative stop, joins every
accepted child, then throws afon::scope_deadline_exceeded. A callable that
ignores its stop token can therefore delay the return. Error precedence is
stable: a body error wins over a child failure, a child failure wins over
deadline expiration, and deadline expiration wins over child cancellation.
All timed scopes share one timer scheduler rather than creating a timer thread
for each scope or runtime.
For uniform collections:
auto hashes = runtime.parallel_map(files, [](std::stop_token stop, auto file) {
return hash_file(file, stop);
});Results preserve input order even though execution order is unspecified. Bulk operations submit work in windows based on the runtime's worker and queue capacity, so retained future handles do not grow with the full input size.
runtime::stats() returns cumulative accepted, completed, failed, and cancelled
counts. It also reports current queued and active work plus monotonic
peak_queued and peak_active high-water marks for runtime sizing.
The queue capacity applies to waiting tasks; running tasks are not included.
admission_policy::blockwaits for capacity.admission_policy::rejectthrowsafon::overload_error.
Blocking runtimes also support per-submission admission budgets:
auto result = runtime.submit_for(std::chrono::milliseconds(25), do_work);
auto other = runtime.submit_until(deadline, do_other_work);If queue capacity is not acquired before the steady-clock deadline, submission
throws afon::admission_timeout and the task is not accepted. Runtime
shutdown still reports afon::runtime_stopped, while a runtime configured
with rejecting admission continues to report afon::overload_error
immediately.
Submission from one of the runtime's own workers executes inline. This prevents a worker from blocking forever after submitting work to its own saturated runtime. Timed nested submissions follow the same rule even if their deadline has expired. Deeply recursive task submission can increase stack use.
shutdown_mode::drain rejects new submissions, runs all accepted tasks, and
joins the workers.
shutdown_mode::cancel_pending additionally settles queued tasks with
afon::task_cancelled and requests cancellation for running tasks.
The destructor uses cancel_pending. Shutdown cannot complete until running
callables return, and shutdown from one of the runtime's worker threads is
rejected.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failureOptional targets:
cmake -S . -B build-bench -DAFON_BUILD_BENCHMARKS=ON
cmake --build build-bench --parallel
./build-bench/afon_throughput_benchmark > results.csv
./build-bench/afon_application_benchmark > application-results.csvThe benchmark performs one warm-up and three measured iterations by default.
It compares direct execution, std::async, and Afon across no-op,
CPU-bound, and short blocking work. Afon runs at 1, 2, and 4 workers with
queue capacities of 1 and 64. Use --tasks and --iterations to adjust run
length.
Output is CSV with platform, compiler, hardware concurrency, workload, executor, runtime dimensions, elapsed nanoseconds, throughput, checksum, and Afon high-water statistics. Every observation validates its checksum before it is reported.
Benchmark results include operating-system scheduling, compiler, power, and storage effects. Compare repeated runs on an otherwise idle machine with the same build and environment; do not treat values from different machines as a runtime regression threshold.
The application benchmark generates deterministic duplicate-file and log datasets from an explicit seed. It covers small files, large files, mostly-unique content, and duplicate-heavy content across configurable worker and queue dimensions. Before timing, it regenerates every selected dataset and checks the trees byte for byte. Each timed application result is also validated against expected counts before its CSV row is emitted.
Use --files, --small-bytes, --large-bytes, --seed, --profiles,
--workers, --queue-capacities, and --iterations to control a run.
--dataset-root writes into a new persistent directory for inspection;
otherwise temporary datasets are removed. Each configuration receives one
untimed warm-up. Filesystem caches, storage devices, background I/O, and
thermal state can dominate results, so compare repeated runs on the same
machine and do not treat the output as a universal performance threshold.
afon-dupes is a complete duplicate-file finder
built on Afon. It recursively discovers files, filters candidates by size,
hashes candidates in parallel, and verifies matching hashes byte for byte.
afon-logs processes Common Log Format files in
parallel and deterministically merges status, endpoint, byte, and malformed-line
aggregates.
afon-manifest creates and verifies deterministic
SHA-256 directory manifests. Its versioned canonical format safely round-trips
UTF-8 paths and verification classifies added, removed, changed, and unreadable
files.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --target afon_dupes_cli --parallel
./build/apps/dupes/afon-dupes --workers 4 --queue-capacity 64 /data
cmake --build build --target afon_logs_cli --parallel
./build/apps/logs/afon-logs --workers 4 /var/log/httpd
cmake --build build --target afon_manifest_cli --parallel
./build/apps/manifest/afon-manifest create /data /backups/data.afon
./build/apps/manifest/afon-manifest verify /data /backups/data.afonSet AFON_BUILD_APPLICATIONS=OFF to omit reference application targets.
Install and consume through CMake:
cmake --install build --prefix /desired/prefixfind_package(afon CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE afon::afon)- Every accepted task settles its future exactly once.
- Submission is safe from multiple threads.
- No task starts after it is cancelled while queued.
- Start and completion order are unspecified.
- Task exceptions are rethrown by
std::future::get. parallel_mappreserves result order.
Afon currently uses a mutex and condition-variable queue. Scheduling internals are intentionally hidden so measured implementations such as work stealing can be introduced without changing callers.
MIT