Skip to main content

ValkarnSemaphore

ValkarnSemaphore is a zero-allocation async semaphore built on ValkarnTask. It works like SemaphoreSlim but integrates natively with the Valkarn Tasks pipeline — no Task allocations, no GC pressure in steady state.

namespace UnaPartidaMas.Valkarn.Tasks

Fast path (uncontended): returns ValkarnTask.CompletedTask immediately — zero allocation. Slow path (contended): waiter nodes are rented from a bounded pool and returned to it on completion — zero GC in steady state.


Constructor​

public ValkarnSemaphore(int initialCount, int maxCount)
ParameterDescription
initialCountNumber of slots available immediately. Must be in [0, maxCount].
maxCountUpper bound for the slot count. Must be > 0.

Properties​

public int CurrentCount { get; } // Available slots right now (thread-safe)

Methods​

WaitAsync​

public ValkarnTask WaitAsync(CancellationToken ct = default)

Acquires one slot asynchronously. Returns a completed ValkarnTask immediately if a slot is available (zero allocation). Suspends the caller until Release() is called if no slot is available. Waiters are served in FIFO order.

Throws OperationCanceledException if ct fires while waiting.

Wait​

public void Wait(CancellationToken ct = default)

Synchronous variant. Blocks the calling thread if no slot is available. Use only when you cannot use await (e.g. non-async entry points).

Release​

public void Release(int releaseCount = 1)

Returns releaseCount slots. Pending waiters are completed in FIFO order, outside the internal lock — continuations never run with the lock held.

Throws InvalidOperationException if releasing would exceed maxCount.

Dispose​

public void Dispose()

Cancels all pending waiters and releases resources. Idempotent.


Usage​

Mutual exclusion (1 slot)​

var sem = new ValkarnSemaphore(initialCount: 1, maxCount: 1);

async ValkarnTask WriteFileAsync(string path, string content, CancellationToken ct)
{
await sem.WaitAsync(ct);
try
{
await File.WriteAllTextAsync(path, content, ct);
}
finally
{
sem.Release();
}
}

Bounded concurrency (N slots)​

// Allow up to 4 concurrent network requests.
var sem = new ValkarnSemaphore(initialCount: 4, maxCount: 4);

async ValkarnTask FetchAsync(string url, CancellationToken ct)
{
await sem.WaitAsync(ct);
try
{
await DownloadAsync(url, ct);
}
finally
{
sem.Release();
}
}

Producer / consumer gate​

// Start with 0 slots — consumer waits until producer signals.
var gate = new ValkarnSemaphore(initialCount: 0, maxCount: 1);

async ValkarnTask ProducerAsync(CancellationToken ct)
{
await ProduceDataAsync(ct);
gate.Release(); // signal consumer
}

async ValkarnTask ConsumerAsync(CancellationToken ct)
{
await gate.WaitAsync(ct); // wait for signal
await ConsumeDataAsync(ct);
}

Thread safety​

All operations are thread-safe. Release() may be called from any thread, including background threads. Completions are dispatched outside the internal lock — a continuation that re-enters the semaphore will not deadlock.


Comparison with SemaphoreSlim​

ValkarnSemaphoreSemaphoreSlim
Fast path allocationZeroZero
Slow path allocationPooled node, zero GCTask allocation
Integrates with ValkarnTaskYesRequires .AsValkarnTask()
IDisposableYesYes
FIFO orderingYesYes
CancellationYesYes