Skip to content

About

Modular C++23 runtime for tasks, schedulers, thread pools, timers, coroutines, and async I/O integration.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

11 Commits

Folders and files

Repository files navigation

modern-runtime

modern-runtime is a modular C++23 runtime for applications and libraries that need schedulers, thread pools, timers, tasks, coroutine-aware execution, and allocator-friendly async composition. It provides a reusable execution foundation so you can build async systems on top of a consistent runtime layer instead of re-solving scheduling, timing, cancellation, continuation chaining, and completion handoff in each project.

Why modern_runtime

Use modern_runtime when you want to:

  • run CPU work, timer work, and completion-based async work through one runtime surface
  • compose pipelines with modern::task<T> using continuations (then, catching, finally) or coroutines (co_await, co_return)
  • propagate scheduler, allocator, stop, and trace context across async boundaries
  • keep execution concerns separate from domain code such as networking, file handling, or protocol layers
  • integrate external I/O backends without rebuilding a full runtime around them

modern_runtime consumes the shared trace contract from modern_trace. Runtime-owned behavior is propagation and environment seeding, not trace wire-format parsing or drain infrastructure.

modern_runtime is the layer below domain-specific async libraries. It owns scheduling, task orchestration, timers, coroutine task infrastructure, PMR-aware allocation points, and runtime adapters. It intentionally does not try to become a second filesystem or networking framework next to modern_io.

What You Get

  • modern.exec: schedulers, executor primitives, and priority-aware execution
  • modern.thread: thread pools, work stealing, bounded queues, and affinity-aware workers
  • modern.timer: delayed and periodic scheduling
  • modern.task: task<T> continuations with then, catching, and finally
  • modern.stream: bounded, backpressured, single-producer/single-consumer streams
  • modern.runtime: umbrella module for adapters, fibers, senders, and coroutine-native runtime tasks
  • modern.memory: a small std::pmr-oriented allocation surface
  • modern.sync and modern.platform: synchronization and platform helpers used by the runtime surface

Build And Run

Requirements:

  • C++23 toolchain
  • CMake 3.28+
  • Clang with module support is the primary validated setup
  • Ninja is used by the included presets

Debug build:

# Source builds resolve modern_trace automatically:
# sibling ../modern_trace when present, otherwise GitHub fetch.
# Override with -DMODERN_TRACE_PROVIDER=package|local|fetch
# and -DMODERN_TRACE_ROOT=../modern_trace.
cmake --preset debug
cmake --build --preset debug
./build/debug/exec_smoke
./build/debug/async_demo
./build/debug/smoke_test

Other presets:

cmake --preset release
cmake --build --preset release

cmake --preset sanitize
cmake --build --preset sanitize

The local validated setup is Clang 18.1.3, Ninja, CMake 3.28.3, and libstdc++ 13. GCC 13 is not an active validation target at the moment.

Add It To Your Project

The repository builds a modern_runtime library target and resolves modern_trace as a sibling or fetched dependency.

add_subdirectory(modern_runtime)

target_link_libraries(my_app PRIVATE modern_runtime)

If you want the full umbrella surface, import modern.runtime. If you want tighter compile-time boundaries, import only the modules you need.

Quick Start

import modern.runtime;

#include <chrono>
#include <iostream>
#include <string>

int main()
{
	using namespace std::chrono_literals;

	modern::thread_pool cpu{4};
	modern::scheduled_executor timers{cpu};

	auto value = modern::submit(cpu, []
		{
			return 21;
		})
		.then([](int x)
		{
			return x * 2;
		})
		.get();

	auto delayed = modern::schedule_after(timers, 100ms, []
		{
			return std::string{"done"};
		})
		.then([](std::string text)
		{
			return text + " after a timer";
		})
		.get();

	std::cout << value << "\n";
	std::cout << delayed << "\n";

	timers.shutdown();
	timers.join();
	cpu.shutdown();
	cpu.join();
}

That is the core usage pattern:

  • create an execution context such as thread_pool or scheduled_executor
  • submit work with modern::submit(...)
  • compose follow-up work with continuations or coroutines
  • wait, stop, or hand results onward at your application boundary

How To Use modern_runtime

Choose The Import Surface

Use the umbrella when you want the complete runtime layer:

import modern.runtime;

Use narrow imports when you want explicit boundaries:

import modern.exec;
import modern.task;
import modern.stream;
import modern.timer;
import modern.thread;
import modern.sync;
import modern.memory;
import modern.platform;

The Task Model

modern::task<T> is the single unified task type. It supports both composition styles:

Continuation style:

auto result = modern::submit(pool, work)
    .then(transform)
    .catching(handle_error)
    .finally(cleanup);

Coroutine style:

modern::task<int> operation()
{
    auto value = co_await modern::submit(pool, work);
    co_return value * 2;
}

Mixed style:

modern::task<int> operation()
{
    auto value = co_await modern::submit(pool, work)
        .then([](int x) { return x * 2; });
    co_return value;
}

All one-shot async producers (submit, schedule_after, schedule_at, bind_io, as_task, I/O adapters) return modern::task<T>.

Start semantics:

  • Coroutine functions returning task<T> start eagerly (run immediately on creation)
  • submit, schedule_after, schedule_at, I/O operations start eagerly (work submitted/armed immediately)

Consumption:

  • task<T> is move-only and single-consumer
  • Consume via get(), co_await, or then()/catching()/finally()
  • Second consumption throws std::logic_error

Cancellation:

  • std::stop_token propagates through task_environment
  • co_await and get() throw operation_cancelled on stopped state

Schedule Work

  • modern::submit(...) runs work on a scheduler or thread pool and returns a task you can compose.
  • modern::schedule_after(...) and modern::schedule_at(...) run delayed work.
  • scheduled_executor::schedule_fixed_rate(...) is the periodic entry point for repeated jobs.

Control Memory, Cancellation, And Execution

  • thread_pool and scheduled_executor can use std::pmr::memory_resource for runtime allocations.
  • task continuations can keep propagating a parent resource or accept an explicit override.
  • runtime adapters accept std::stop_token so cancelled work can fail early before execution starts.
  • schedulers support modern::scheduler_priority with high, normal, and low.
  • thread pools can pin workers to configured CPU affinities.

Integrate External I/O Instead Of Rebuilding It

  • modern::bind_io<R>(...) adapts completion-based I/O starts into task<R>.
  • modern::adapt_modern_io(service) wraps modern_io-like services that expose start(...) or submit(...).
  • modern::adapt_modern_io_uring(service) and modern::adapt_modern_io_poller(service) add backend-aware integration for io_uring, epoll, and kqueue style services.

This keeps the ownership line clear: modern_runtime provides the runtime substrate, while modern_io or another domain layer owns file, socket, and transport semantics.

Bridge Other Async Models

  • modern::as_task<R>(sender, scheduler, resource) adapts senders into task<R>.
  • modern::fiber_scheduler provides cooperative scheduler-based fiber requeueing and grouped stop/join behavior.
  • modern::task_environment carries scheduler, frame allocator, trace context, and stop state through coroutine and continuation execution.

Produce And Consume Streams

modern::stream<T> is a move-only, bounded SPSC stream. next() returns a task containing either a value, clean EOF, or a typed stream_error. Producers created through make_stream() receive a move-only stream_source<T>; its send() operation applies backpressure while the configured buffer is full.

auto values = modern::make_stream<int>(1,
  [](modern::stream_source<int> source) -> modern::task<void>
  {
    if (auto sent = co_await source.send(1); !sent)
      co_return;
    if (auto sent = co_await source.send(2); !sent)
      co_return;
  });

while (auto item = values.next().get())
{
  if (!*item)
    break;
  std::cout << **item << "\n";
}

Normal producer completion closes and drains accepted values. Failure and cancellation discard buffered values immediately. The complete contract is in docs/stream_contract.md.

Repository Guide

About

Modular C++23 runtime for tasks, schedulers, thread pools, timers, coroutines, and async I/O integration.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages