.NET
C#
blocking queue
multithreading
concurrent programming

Creating a blocking QueueT in .NET?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A blocking queue is a classic producer-consumer tool. Producers add items, consumers remove items, and either side waits when progress is temporarily impossible. In .NET, the standard building block for this pattern is usually BlockingCollection<T> rather than a custom queue with manual locks and condition variables.

Using the built-in collection keeps the code shorter, safer, and easier to stop cleanly when work is finished.

Why BlockingCollection<T> Fits The Pattern

BlockingCollection<T> wraps an IProducerConsumerCollection<T> and adds blocking and completion semantics. That means producers can block when a bounded queue is full, and consumers can block when no items are available yet.

The default underlying store is ConcurrentQueue<T>, so it behaves like a queue out of the box.

Basic Producer-Consumer Example

The example below creates a bounded blocking queue with one producer and one consumer:

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Threading;
4using System.Threading.Tasks;
5
6class Program
7{
8    static void Main()
9    {
10        using var queue = new BlockingCollection<int>(boundedCapacity: 3);
11
12        var producer = Task.Run(() =>
13        {
14            for (int i = 1; i <= 5; i++)
15            {
16                queue.Add(i);
17                Console.WriteLine($"Produced {i}");
18                Thread.Sleep(200);
19            }
20
21            queue.CompleteAdding();
22        });
23
24        var consumer = Task.Run(() =>
25        {
26            foreach (var item in queue.GetConsumingEnumerable())
27            {
28                Console.WriteLine($"Consumed {item}");
29                Thread.Sleep(500);
30            }
31        });
32
33        Task.WaitAll(producer, consumer);
34    }
35}

When the queue reaches capacity, Add blocks until the consumer removes something. When the queue is empty, GetConsumingEnumerable waits until new data arrives or adding is completed.

Cancellation Support

In real services, blocking forever is rarely acceptable. You often want cancellation:

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Threading;
4
5var queue = new BlockingCollection<string>(2);
6var cts = new CancellationTokenSource();
7
8try
9{
10    queue.Add("task-1", cts.Token);
11    string item = queue.Take(cts.Token);
12    Console.WriteLine(item);
13}
14catch (OperationCanceledException)
15{
16    Console.WriteLine("operation canceled");
17}
18finally
19{
20    queue.Dispose();
21    cts.Dispose();
22}

Cancellation tokens are especially useful when the queue is part of a hosted service that needs graceful shutdown behavior. They also make the queue easier to integrate with the rest of a Task-based application.

Why Not Build It From Scratch

You can implement a blocking queue manually with lock, Monitor.Wait, and Monitor.Pulse, but that is usually unnecessary. The custom version is easy to get wrong, especially around shutdown semantics, cancellation, fairness, or exception safety.

Unless you need a very specialized queue behavior, BlockingCollection<T> is the pragmatic choice.

When To Use Channels Instead

For newer asynchronous workflows, System.Threading.Channels can be an even better fit, especially when consumers are async methods. Channels are designed around asynchronous producers and consumers, whereas BlockingCollection<T> is oriented toward blocking thread-based coordination.

So the right answer depends on the style of your application. For classic multithreaded producer-consumer code, BlockingCollection<T> remains an excellent tool.

Common Pitfalls

The most common mistake is forgetting to call CompleteAdding(). Without it, consumers using GetConsumingEnumerable() may wait forever because they have no signal that production is finished.

Another pitfall is mixing blocking queue operations into an async codebase without thinking about thread usage. Blocking calls tie up threads. If the workflow is deeply asynchronous, channels may be more appropriate.

A third issue is writing a custom blocking queue before trying the standard library. Most homegrown versions duplicate features that .NET already provides and often handle shutdown less reliably.

Summary

  • 'BlockingCollection<T> is the standard .NET tool for a blocking queue pattern.'
  • It supports bounded capacity, blocking producers and consumers, and graceful completion.
  • 'GetConsumingEnumerable() is a simple way to consume until production is finished.'
  • Add cancellation when the queue participates in service shutdown or timeout logic.
  • Prefer built-in concurrent primitives over hand-rolled locking code.

Course illustration
Course illustration

All Rights Reserved.