A high-throughput C# threading engine built on a Token/Ticket model. Tokens handle IO (async), Tickets handle CPU (sync). The engine routes work through a staged pipeline that prefers IO leads over CPU leads, with formation ordering to prevent starvation.
Verified: 1.29M items/sec across 21M items, 0 errors, 8 cores.
Token — an async IO work item. Wraps a Func<SharpToken, Task>. Carries a CancellationToken via implicit conversion so standard .NET async APIs work without unwrapping.
Ticket — a synchronous CPU work item. Wraps an Action<SharpTicket>. Runs on a dedicated thread pool via Task.Run so it never blocks IO workers.
Group — a lead plus any number of children. The lead claims a core slot; children inherit the core and group identity. The group lifecycle gates completion: no group is done until the lead and all children resolve.
Formation — items within a group execute in sequence-number order. The FormationScheduler enforces this with O(1) checks against sorted pending sets.
var corePool = new CorePool();
var registry = new OperationRegistry(corePool);
var cts = new CancellationTokenSource();
var tokenQueue = new AdmissionQueue<Token> (corePool);
var ticketQueue = new AdmissionQueue<Ticket>(corePool);
var tokenizer = new Tokenizer (tokenQueue, registry);
var ticketCreator = new TicketCreator(ticketQueue, registry);
// Start background drain loops — these run for the lifetime of the engine
var background = Task.WhenAll(
tokenQueue .DrainAsync(cts.Token),
ticketQueue.DrainAsync(cts.Token),
corePool .StartAsync(cts.Token));
// ... do work ...
// Shutdown
cts.Cancel();
try { await background; } catch { }// IO-lead group
var g = new BatchGroup(_tokenizer.CreateGroup());
g.Add(await tokenizer.CreateLeadAsync(
async st => { /* your IO work */ }, g.Lifecycle));
g.Add(await tokenizer.CreateChildAsync(
async st => { /* child IO */ }, g.Lifecycle));
g.Add(await ticketCreator.CreateChildAsync(
st => { /* child CPU */ }, g.Lifecycle));
// CPU-lead group
var g = new BatchGroup(ticketCreator.CreateGroup());
g.Add(await ticketCreator.CreateLeadAsync(
st => { /* your CPU work */ }, g.Lifecycle));Rule: add the lead first, then children. The BatchProcessor seals the lifecycle — don't seal manually.
var result = await BatchProcessor.RunAsync(
() => BuildGroupA(),
() => BuildGroupB(),
() => BuildGroupC());
// All groups race to their cores simultaneously.
// RunAsync returns when every item in every group reaches Resolved or Failed.Both structs are passed to your work delegate at execution time. CoreId and Group are already resolved — read at registration, baked in before your code runs.
async st =>
{
Console.WriteLine($"Running on Core {st.CoreId}, Group {st.Group}");
await SomeAsyncApi(st); // implicit CancellationToken conversion
}
st =>
{
Console.WriteLine($"CPU work on Core {st.CoreId}, Group {st.Group}");
}FORMED → CREATED → ADMITTED → CHECKED → STARTED → COMPLETED → RESOLVED
↘ FAILED
- FORMED — object allocated, not yet registered
- CREATED — registered, work delegate attached
- ADMITTED — enqueued to the admission queue
- CHECKED — entered the staging gate (IO/CPU comparand runs here)
- STARTED — executing
- COMPLETED — work delegate returned
- RESOLVED — fully done, timestamps available
- FAILED — exception thrown during execution
Timestamps are available at each state: CreatedAt, AdmittedAt, CheckedAt, StartedAt, CompletedAt, ResolvedAt. Derived spans: QueueWait, StagingWait, Elapsed, TotalLifetime.
var loadTest = new LoadTest(tokenizer, ticketCreator);
// Named presets
await loadTest.RunAsync(LoadTestConfig.Light); // 5 waves × 12 groups × 4 children
await loadTest.RunAsync(LoadTestConfig.Default); // 50 waves × 120 groups × 40 children
await loadTest.RunAsync(LoadTestConfig.Heavy); // 100 waves × 500 groups × 100 children
await loadTest.RunAsync(LoadTestConfig.Stress); // 200 waves × 1000 groups × 200 children
// Custom
await loadTest.RunAsync(new LoadTestConfig
{
Waves = 10,
GroupsPerWave = 350,
ChildrenPerGrp = 6000,
});var statementTest = new StatementTest(tokenizer, ticketCreator);
await statementTest.RunAsync(waves: 10, groupsPerDomain: 25);Four domains run concurrently per wave: Document, Cache, Log, Index. Each domain has a distinct lead type and writes to shared concurrent state. Use this to verify correctness under realistic mixed IO/CPU load.
csproj settings that matter:
<ServerGarbageCollection>true</ServerGarbageCollection>
<GarbageCollectionAdaptationMode>0</GarbageCollectionAdaptationMode>
<TieredPGO>true</TieredPGO>ServerGarbageCollection— dedicated GC thread per core; eliminates stop-the-world pauses from worker threads. Most impactful single setting.GarbageCollectionAdaptationMode=0— disables dynamic GC strategy switching; stabilises wave-to-wave variance under bursty load.TieredPGO— profile-guided JIT recompilation on hot paths; wave 1 to wave N throughput delta compresses significantly.
Letter pool — OperationRegistry maintains a 65536-char pool for group identity. At that depth, pool exhaustion under realistic concurrent group counts is not a concern. If you hit it, the engine throws InvalidOperationException with a clear message.
Formation scheduler — O(1) rule evaluation via SortedSet<int>. No list scans. WorkerInbox uses HashSet<T> for O(1) completion removal.
- Max concurrent groups: 65536 (letter pool depth)
- Max workers per core: configurable via
CorePool(workersPerCore:), default 4 - Group letter cycles: single
char, wraps at Unicode boundary past 65471 - HT detection: not yet implemented;
CorePoolusesEnvironment.ProcessorCount(logical cores)