CLR & Concurrency · Lesson 18

ConcurrentQueue peek does not reserve an item

Treat each concurrent collection operation as its own contract.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

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 concurrent collection supplies atomicity for its documented operations, not for every workflow assembled from several calls. TryPeek observes an item without reserving it, while TryDequeue attempts a removal. The portable companion emphasizes the success boolean because zero can be both a legitimate dequeued value and the default out value for an empty queue. The external project then coordinates two actual consumers that peek the same first item before either removes anything. Their later removals are distinct, demonstrating the boundary between a useful observation and exclusive ownership of an item.

Trace the portable program

The portable queue contains one integer zero. Its first TryDequeue succeeds and returns that real zero; the next TryDequeue fails and sets the missing value to zero. Output is first:True:0 and empty:False:0. Looking only at the out integer would conflate success with absence. In the desktop experiment both consumers first observe zero, but the two removals produce the two distinct queued values and leave the queue empty. It reports stable relationship checks rather than guessing which worker removes zero. A common wrong answer assumes the item seen by TryPeek has been claimed for that caller.

Execute the actual desktop lab

The desktop consumers both peek zero while a barrier prevents either removal from occurring early. They then perform separate TryDequeue calls, removing the two distinct stored integers. The stable report checks that both peeks matched the initial head, that removals are distinct, and that the queue is empty after joining. It does not assign a particular integer to a particular worker. FIFO item ordering and worker scheduling are different questions. This controlled experiment demonstrates the nonreservation property directly, while the portable program establishes the success/out-value distinction without creating threads or pretending a local peek claims ownership.

Pitfalls and boundary limits

Count, IsEmpty, and TryPeek can provide observations that become stale before another operation. A workflow that checks Count and then assumes a later removal cannot fail has not acquired a reservation. Use the actual removal result to determine ownership. Once an item has been removed, successful processing, retry, or durable acknowledgment is another boundary: ConcurrentQueue does not restore a failed item or implement a message broker's delivery protocol automatically. The example deliberately has no external effects and no processing failure. Keep those additional lifecycle guarantees explicit instead of extending queue thread safety to everything the consumer does afterward.

Practice one: isolate behavior

First enqueue a nonzero value in the portable program and predict that the first out value changes while the empty out value remains the integer default zero. Hint: the success boolean still decides presence. Then enqueue two zero values and verify that two successful removals precede failure, even though every observed integer is zero. This challenges any code using zero as an empty sentinel. Keep each observation's boolean and value paired in the report rather than printing a list of integers that erases the operation outcomes.

Practice two: design and verify

For another exercise, replace the desktop peek-plus-remove assumption with a consumer that claims ownership only after successful TryDequeue. Record which items were actually removed and verify no duplicate ownership across bounded workers. Hint: do not use a prior Count check as a promise. Then define a processing-failure policy, including whether a removed item may be retried and how duplicate effects are controlled. If durable delivery is required, choose and test a real persistence or messaging boundary instead of treating an in-memory concurrent collection as an acknowledgment protocol.

Review the evidence

Review a concurrent-collection workflow by identifying each atomic operation and every gap between observation, ownership, and processing. The portable two-line result distinguishes legitimate zero from absence. The external relationships prove that two peeks can precede different removals, without predicting scheduler order or fairness. These are precise useful contracts, not a complete queue-consumer system. Preserve the result of the operation that actually transfers ownership, and handle completion, cancellation, retries, and external effects according to the surrounding application's requirements rather than inferring them from FIFO storage alone. With concurrent producers, decide whether the application needs per-producer order, accepted queue order, or an external sequence number; those contracts do not automatically coincide.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

ConcurrentQueue peek does not reserve an item — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. 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.csDownload 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 queue = new ConcurrentQueue<int>(new[] { 0, 1 });
using var peeked = new Barrier(2);
int[] seen = new int[2], removed = new int[2];
void Consume(int index)
{
    if (!queue.TryPeek(out seen[index])) throw new InvalidOperationException();
    if (!peeked.SignalAndWait(TimeSpan.FromSeconds(5))) throw new TimeoutException();
    if (!queue.TryDequeue(out removed[index])) throw new InvalidOperationException();
}
OwnedThreads.Run(() => Consume(0), () => Consume(1));
Console.WriteLine($"same-peek:{seen[0] == 0 && seen[1] == 0}");
Console.WriteLine($"distinct-removals:{removed.Distinct().Count() == 2}");
Console.WriteLine($"remaining:{queue.Count}");

public static class OwnedThreads
{
    public static void Run(params Action[] actions)
    {
        var errors = new ConcurrentQueue<Exception>();
        var threads = actions.Select(action => new Thread(() => { try { action(); } catch (Exception error) { errors.Enqueue(error); } }) { IsBackground = true }).ToArray();
        foreach (var thread in threads) thread.Start();
        foreach (var thread in threads) if (!thread.Join(TimeSpan.FromSeconds(8))) throw new TimeoutException("Owned worker did not finish");
        if (!errors.IsEmpty) throw new AggregateException(errors);
    }
}
RuntimeLab.csprojDownload 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: same-peek:True distinct-removals:True remaining:0 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 queue = new ConcurrentQueue<int>();
queue.Enqueue(0);
bool first = queue.TryDequeue(out int value);
bool empty = queue.TryDequeue(out int missing);
Console.WriteLine($"first:{first}:{value}");
Console.WriteLine($"empty:{empty}:{missing}");
Open C# practice

C# practice output

first:True:0
empty:False:0
Try this
The success boolean distinguishes a real zero from an empty result. Peeking then removing is not one reserved-item operation.

Further reading

Interview questions for this lesson