Mutable project understanding for agents. This file evolves. The stable, harness-loaded constitution is
AGENTS.md— keep volatile detail here, not there.Last reviewed: 2026-06-17
Helios.DedicatedThreadPool is a small concurrency primitive: an instanced,
dedicated thread pool that isolates work onto its own fixed set of threads so a
noisy workload cannot starve the shared CLR ThreadPool (the "noisy neighbor"
problem). It ships as a single source file (Helios.Concurrency.DedicatedThreadPool.cs)
via a content-only NuGet package, with types internal by default so it can be
embedded opaquely across dependent projects.
Public surface today:
Helios.Concurrency.DedicatedThreadPool— the pool (QueueUserWorkItem,Dispose).Helios.Concurrency.DedicatedThreadPoolTaskScheduler— TPLTaskSchedulerfacade.Helios.Concurrency.DedicatedThreadPoolSettings— config (thread count, name, deadlock timeout, exception handler).- Internal:
UnfairSemaphore(ported from legacy CoreCLRwin32threadpool.h), a single sharedConcurrentQueuework queue, per-thread consuming workers.
Akka.NET once used this pool for system actors, then retreated to the .NET
ThreadPool because the dedicated pool had two fatal gaps:
- No hill-climbing — thread count is fixed; it cannot adapt to load.
- Inefficient waiting — no modern adaptive spin-then-park / LIFO-warm-thread wake discipline, and no work-stealing.
Closing those two gaps is the entire reason this project is being revived.
- Fixed thread count. No thread injection / hill-climbing controller.
- One shared global queue. No per-worker local (LIFO) deques, no work-stealing, no per-connection/per-key affinity.
- Dated waiting.
UnfairSemaphorespin-then-park is serviceable but predates the runtime'sLowLevelLifoSemaphore+ adaptive-spin design. - Legacy toolchain. FAKE 4.61.2
build.fsx(still self-identifies asAkka.Streams.Kafka— copy-paste residue), vendorednuget.exev4.0.0, Azure Pipelines on dead images (vs2017-win2016,ubuntu-16.04). Noglobal.json,Directory.Build.props, CPM, or GitHub Actions.
- Modernize to 2026 standards modeled on
akkadotnet/build-system-template:.slnx,Directory.Build.props, central package management (Directory.Packages.props),global.json,.config/dotnet-tools.json, GitHub Actions CI/CD, apwshbuild.ps1(retiring FAKE), coverlet coverage. - Deliver a genuinely modern dedicated pool that hill-climbs, waits
efficiently, and work-steals with affinity — i.e. phases P1–P2 of the
IoExecutor spec (the reusable pool core +
HillClimbingController). This repo owns the standalone primitive; Akka-specific facets (Dispatcher, PipeScheduler, socket-completion routing — spec P3–P5) live in the Akka.NET repo, not here.
| Decision | Choice | Consequence |
|---|---|---|
| Repo scope | Standalone primitive + framework-agnostic adapters (TaskScheduler, SynchronizationContext, IThreadPoolWorkItem). No Akka dependency. |
Akka integration stays in akka.net and consumes this. |
| Branding/packaging | Keep Helios.Concurrency namespace + source-ship (content-only NuGet, internal types). |
Backward compatible; distribution model unchanged. |
| Target frameworks | net10.0 only (netstandard2.0 deferred, not abandoned). |
Free use of modern runtime APIs/intrinsics; no cross-TFM #if branching for now. |
| Test/bench/CI | xUnit + BenchmarkDotNet + GitHub Actions (migrating from NUnit/NBench/Azure). | Aligns with the dotnet-skills + build-system-template tooling. |
To de-risk the rewrite, the library targets net10.0 only for now;
netstandard2.0 is deferred, not abandoned. While single-target:
- Use modern runtime APIs/intrinsics freely (modern parking,
Thread.UnsafeStart, hardwarePause, newerInterlocked/Volatile) with no#ifbranching. - The library still source-ships as one
.csfile via a content-only package.
Tradeoff to remember: writing freely against net10.0 means re-adding
netstandard2.0 later will require retrofitting polyfills / #if NET10_0_OR_GREATER
guards around those modern APIs. Keep hot-path API choices loosely noted so that
retrofit stays mechanical if portability is reinstated.
The pool is four cooperating subsystems, each with a battle-tested (but internal)
dotnet/runtime reference we reimplement from public APIs: cheap-waiting/parking,
work-stealing deque, false-sharing-safe layout, and hill-climbing + a separate
starvation injector. Full implementation digest (per-primitive guidance, parameters,
correctness traps, sources): memorizer eb4916e3-fa71-49dd-8763-237c5a47c070.
| Decision | Choice |
|---|---|
| Affinity semantics | Locality hint, not ordering. One Chase-Lev deque/worker; bias a key's initial placement to hash(key) % workers, but the item stays steal-eligible. Per-key ordering is an adapter concern. → hill-climbing changes the worker count freely (only new work re-hashes; in-flight work is steal-eligible), so there is no rebalancing problem in the core. |
| Runtime model to mirror | net10/main (single-Signal() + managed LIFO blocker-stack), not release/8.0 (Release(N) + IOCP/PAL). |
| Blocking-awareness (v1) | Ship the GateThread starvation injector + expose NotifyBlocked/Unblocked hooks; defer the full cooperative-blocking delay-stepped ramp. (A blocked worker reports zero completions, so the throughput controller alone would remove threads when it should add — the injector is mandatory and owns cold-start ramp.) |
| Windows IOCP fast-path | Deferred. Managed LIFO blocker-stack is the portable baseline. |
| Worker-count cap | Respect cgroup CPU quota, not just Environment.ProcessorCount (container constraint; over-threading under a quota → throttling). |
The standalone primitive must, on a CPU-bound microbenchmark, match the .NET
ThreadPool throughput within ~5% (prove no compute regression), demonstrate
hill-climbing convergence to an optimal active-thread count within ~2s of a load
step, and show park→run latency ≤ the .NET ThreadPool's. Full goal set (G1–G9,
which include the Akka-transport-level goals owned by the akka.net repo) lives in the
spec; the repo-local subset is G8 (convergence + microbench parity) and the parking /
work-stealing correctness gates.
The implementation-grade design lives in the memorizer MCP server, not in this repo. Agents doing pool/perf work must consult it:
2c734cfb-0fb3-42e2-a855-fbea6a56ce83— the IoExecutor spec (full design, algorithms, API, goals G1–G9, validation protocol, phases P1–P6, risks/dead-ends).- Referenced by the spec:
eb52d56b(profiling proof),8ce6bcc0(condensed plan),dae34f6d(benchmark discipline — read before any perf claim), and rejected levers73857988,b856f384,4d6e4f93,741216a6(do not re-attempt these). eb4916e3-fa71-49dd-8763-237c5a47c070— implementation design digest for the standalone primitive (the four subsystems, parameters, correctness traps, the StackExchange.Redis #3060 evidence, measurement methodology). Read before Phase 3/4.
- Do not remove read-ahead pumps (−2.6x;
73857988). - Do not use
PipeScheduler.Inlineor hand-roll an SPSC pipe (−45%;b856f384). - Do not over-thread (DotNetty-style) nor under-thread (starvation).
- Outbound stream-source coalescing is dead (
4d6e4f93); stage fusion targets the wrong layer (741216a6). (These are Akka-transport concerns, listed for completeness.) - Never put a lock on the schedule/enqueue hot path, and never use a single shared
Monitoras the park/wake primitive. StackExchange.Redis #3060's screenshots show exactly this (DedicatedThreadPoolPipeScheduler.Schedule→Monitor.Enter) burning 31–50% of global CPU in lock contention on 2-core nodes. → lock-free Chase-Lev enqueue + packed-CAS / LIFO-blocker-stack park;Monitor.Contentionmust be ~0. - Don't route socket-IO completions onto the pool via
SynchronizationContexton Linux — async completions hit the global .NET ThreadPool regardless (andSOCKETS_INLINE_COMPLETIONS=1made "no measurable difference" per mgravell). True co-location needs raw SAEA scheduling or a dedicated sync read thread — an adapter concern, not the core primitive.
- Default branch:
dev. Remote:github.com/Aaronontheweb/DedicatedThreadPool(origin);github.com/helios-io/DedicatedThreadPool(upstream). - GitHub Issues are disabled on this repo →
IMPLEMENTATION_PLAN.mdis the canonical work tracker, not Issues. - License: Apache 2.0. Copyright Roger Alsing, Aaron Stannard, Jeff Cyr.