Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The runtime contract
A channel has related but distinct admission, buffered-data, and terminal-completion states. Completing its writer ends further admission, but already accepted values can remain available to the reader. A supplied completion failure must also be observed through the terminal task rather than silently discarded after draining. This portable program uses the actual System.Threading.Channels implementation, with capacity two and no OS threads. It is not a queue model claiming to implement channel semantics. The desktop project executes the same bounded protocol, separate from any claim about distributed messaging, durable queues, or production worker shutdown.
Trace the portable program
Two and four are admitted before the writer is completed with a controlled InvalidOperationException. TryRead then drains those accepted values in order, so the first line is items:2,4. Awaiting Reader.Completion observes the real terminal failure and prints failure:InvalidOperationException. A later TryWrite is rejected, producing write-after:False. Draining the values did not convert faulted completion into success. A common wrong answer assumes completion throws away buffered work, or treats cancellation of one reader's wait as permission to close the entire channel owned by all producers and consumers.
Execute the actual desktop lab
The desktop project executes the same actual channel protocol as the portable program. Capacity is two, both writes occur before completion, and no concurrent producer is invented. Reader.Completion represents the terminal reader state and is observed after buffered data is removed. The chosen error is an ordinary InvalidOperationException, so the terminal task faults rather than merely reporting successful exhaustion. Different read APIs can expose closed or faulted state through their own documented shapes, which is why the baseline deliberately observes the Completion task explicitly. The result is bounded lifecycle evidence, not a claim of worker throughput or durable message delivery.
Pitfalls and boundary limits
An empty queue can mean temporarily no data, permanently completed, or faulted after earlier data was drained. A consumer must use the appropriate channel protocol to distinguish those states. Writer ownership also matters when several producers exist: one producer finishing its own work does not necessarily authorize it to complete a channel still used by others. Canceling a single WaitToReadAsync or ReadAsync wait is another separate event. Backpressure and drop policies are configuration contracts, not implied by the word bounded. The baseline uses the ordinary waiting capacity policy and explicit completion to keep those assumptions visible.
Practice one: isolate behavior
First replace the completion error with successful completion and predict the same item line but no failure line, with later writes still rejected. Hint: remove or adjust the error observation deliberately; draining data alone does not determine terminal success. Then complete a fresh empty channel and observe its terminal task without expecting a fabricated item. Keep the output tied to the exact completion owner and error value supplied. These scenarios distinguish admission ending, buffered data, and terminal outcome instead of treating every empty read as the same kind of completion.
Practice two: design and verify
For another exercise, add two bounded producers with one explicit coordinator responsible for completing the writer only after both finish. Define how producer failure reaches the reader and ensure every owned operation is awaited. Hint: a producer should not close shared admission simply because its personal input ended. Add cancellation of one waiting reader and verify the channel can still accept and deliver data to an authorized consumer afterward. Keep those local lifetime tests separate from any production retry, acknowledgment, or durability guarantee that requires a real messaging or storage system.
Review the evidence
Review a channel pipeline by naming admission policy, producer count, completion owner, reader drain responsibility, cancellation scope, and terminal-error observation. The three baseline lines establish accepted data followed by a real failure and rejected later admission. The external run confirms the same BCL lifecycle without pretending to deploy a broker. A complete worker system needs further decisions about unfinished work, shutdown deadlines, and effects after removal. Preserve those boundaries explicitly so channel completion is a well-owned protocol transition rather than a convenient method call whose consequences every participant interprets differently. Observe terminal tasks even when individual item processing appeared successful.
Channels own completion and terminal failure — desktop runtime lab
Set up the SDK project
- Save both files at their exact paths in a fresh owned directory.
- Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
- Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.
Download complete SDK project ↗
Project files
Program.cs
Download file ↗using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
var channel = System.Threading.Channels.Channel.CreateBounded<int>(2);
if (!channel.Writer.TryWrite(2) || !channel.Writer.TryWrite(4)) throw new InvalidOperationException();
channel.Writer.TryComplete(new InvalidOperationException("controlled"));
var items = new List<int>();
while (channel.Reader.TryRead(out int item)) items.Add(item);
Console.WriteLine("items:" + string.Join(",", items));
try { await channel.Reader.Completion; } catch (InvalidOperationException error) { Console.WriteLine($"failure:{error.GetType().Name}"); }
Console.WriteLine($"write-after:{channel.Writer.TryWrite(6)}");
RuntimeLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>false</PublishAot>
</PropertyGroup>
</Project>
Expected result
Exact behavioral stdout on the stated desktop lab: items:2,4 failure:InvalidOperationException write-after:False Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
var channel = System.Threading.Channels.Channel.CreateBounded<int>(2);
if (!channel.Writer.TryWrite(2) || !channel.Writer.TryWrite(4)) throw new InvalidOperationException();
channel.Writer.TryComplete(new InvalidOperationException("controlled"));
var items = new List<int>();
while (channel.Reader.TryRead(out int item)) items.Add(item);
Console.WriteLine("items:" + string.Join(",", items));
try { await channel.Reader.Completion; } catch (InvalidOperationException error) { Console.WriteLine($"failure:{error.GetType().Name}"); }
Console.WriteLine($"write-after:{channel.Writer.TryWrite(6)}");
C# practice output
items:2,4 failure:InvalidOperationException write-after:False
Writer completion ends admission; buffered values and terminal failure still need observation. Canceling one wait is not completing the channel.