Skip to content

Repository files navigation

Afon

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.

Why Afon

  • Thread-safe submission from multiple producers
  • Bounded queue with blocking or rejecting admission
  • Move-only callables and std::future results
  • Cooperative cancellation through std::stop_token
  • Structured scopes that join children and cancel siblings after failure
  • Ordered parallel_map and joined parallel_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

Basic Use

#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.

Structured Work

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 Statistics

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.

Backpressure

The queue capacity applies to waiting tasks; running tasks are not included.

  • admission_policy::block waits for capacity.
  • admission_policy::reject throws afon::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 Contract

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.

Build

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure

Optional 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.csv

The 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.

Practical Applications

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.afon

Set AFON_BUILD_APPLICATIONS=OFF to omit reference application targets.

Install and consume through CMake:

cmake --install build --prefix /desired/prefix
find_package(afon CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE afon::afon)

Guarantees

  • 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_map preserves 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.

License

MIT

About

A bounded structured task runtime for C++20

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages