Project · Runtime
Owned channel and lease contracts
Run six real bounded Channel and SemaphoreSlim ownership scenarios without OS threads, arbitrary sleeps, or invented worker behavior.
All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.
Program.cs
var checks = await PipelineChecks.RunAsync();
foreach (var check in checks) Console.WriteLine($"{check.Name}:{(check.Passed ? "PASS" : "FAIL")}");
Console.WriteLine($"passed:{checks.Count(check => check.Passed)}/{checks.Count}");
return checks.All(check => check.Passed) ? 0 : 1;
Runtime/Lease.cs
public sealed class Lease : IAsyncDisposable
{
private SemaphoreSlim? gate;
private Lease(SemaphoreSlim gate) { this.gate = gate; }
public static async ValueTask<Lease> TakeAsync(SemaphoreSlim gate) { await gate.WaitAsync().ConfigureAwait(false); return new Lease(gate); }
public ValueTask DisposeAsync() { Interlocked.Exchange(ref gate, null)?.Release(); return ValueTask.CompletedTask; }
}
Checks/PipelineChecks.cs
using System.Threading.Channels;
public static class PipelineChecks
{
public static async Task<List<(string Name, bool Passed)>> RunAsync()
{
List<(string Name, bool Passed)> checks = [];
var success = Channel.CreateBounded<int>(2);
success.Writer.TryWrite(2); success.Writer.TryWrite(4); success.Writer.TryComplete();
var items = new List<int>();
await foreach (int item in success.Reader.ReadAllAsync()) items.Add(item);
await success.Reader.Completion;
checks.Add(("drain", items.SequenceEqual(new[] { 2, 4 }) && success.Reader.Completion.IsCompletedSuccessfully));
var closed = Channel.CreateBounded<int>(1);
closed.Writer.TryComplete(); await closed.Reader.Completion;
checks.Add(("write-after", !closed.Writer.TryWrite(7)));
var bounded = Channel.CreateBounded<int>(2);
bool capacity = bounded.Writer.TryWrite(1) && bounded.Writer.TryWrite(2) && !bounded.Writer.TryWrite(3);
while (bounded.Reader.TryRead(out _)) { }
bounded.Writer.TryComplete(); await bounded.Reader.Completion;
checks.Add(("capacity", capacity));
var canceled = Channel.CreateBounded<int>(1);
using var source = new CancellationTokenSource(); source.Cancel();
bool observed = false;
try { await canceled.Reader.ReadAsync(source.Token); } catch (OperationCanceledException error) { observed = error.CancellationToken == source.Token; }
bool admitted = canceled.Writer.TryWrite(7); int retained = await canceled.Reader.ReadAsync();
canceled.Writer.TryComplete(); await canceled.Reader.Completion;
checks.Add(("wait-cancel", observed && admitted && retained == 7));
var failed = Channel.CreateBounded<int>(1);
failed.Writer.TryWrite(9); failed.Writer.TryComplete(new InvalidOperationException("controlled"));
bool drained = failed.Reader.TryRead(out int buffered) && buffered == 9;
bool terminal = false;
try { await failed.Reader.Completion; } catch (InvalidOperationException) { terminal = true; }
checks.Add(("failure-drains", drained && terminal && !failed.Writer.TryWrite(10)));
using var gate = new SemaphoreSlim(1, 1);
var lease = await Lease.TakeAsync(gate);
await lease.DisposeAsync(); await lease.DisposeAsync();
checks.Add(("lease-once", gate.CurrentCount == 1));
return checks;
}
}
ConsoleLab.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>