Is there a data structure in C like a ConcurrentQueue which allows me to await an empty queue until an item is added?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
ConcurrentQueue<T> is thread-safe, but it is not awaitable. If you want consumer code to pause asynchronously until a new item arrives, you need a queue plus a signaling mechanism, or a higher-level abstraction that already combines both ideas.
Why ConcurrentQueue<T> Is Not Enough
ConcurrentQueue<T> solves safe enqueue and dequeue operations across threads. What it does not provide is an async wait primitive that says, "resume me when the queue becomes non-empty."
That means code like this is a bad pattern:
This loop either spins wastefully or forces you to insert artificial delays. Neither is a good consumer design.
The Modern Answer: Channel<T>
In modern .NET, System.Threading.Channels.Channel<T> is usually the best fit for an awaitable producer-consumer queue.
The reader waits asynchronously when there is no item and resumes when data arrives. That is exactly the behavior people often wish ConcurrentQueue<T> provided.
A Custom Awaitable Queue With SemaphoreSlim
If you need to build your own abstraction, the usual pattern is ConcurrentQueue<T> plus SemaphoreSlim.
This works because:
- the queue stores the data safely
- the semaphore tracks how many items are available
- '
WaitAsyncsuspends the consumer without busy-waiting'
For many applications, this is perfectly adequate. Channel<T> is still preferable when available because it handles more edge cases and completion semantics for you.
What About BlockingCollection<T>?
BlockingCollection<T> can block until an item is available, but it is built around synchronous blocking rather than await. It is useful in thread-based code, but it is not the best choice when you want fully asynchronous consumers.
So the rough decision rule is:
- synchronous worker threads:
BlockingCollection<T>can be fine - async producer-consumer code: prefer
Channel<T> - custom minimal wrapper:
ConcurrentQueue<T>plusSemaphoreSlim
Cancellation and Shutdown Matter Too
Real queues usually need more than item delivery. Consumers may need to stop cleanly when the application shuts down. Channel<T> has built-in completion semantics, while a custom queue should usually accept a CancellationToken and define clearly what happens when no more items will arrive.
Common Pitfalls
The biggest pitfall is confusing thread-safe access with awaitable consumption. A collection can be safe for multiple threads and still offer no efficient way to wait asynchronously for new data.
Another common mistake is writing a polling loop with Task.Delay. That works in demos but adds latency, wastes wakeups, and becomes awkward under load.
Developers also forget completion semantics. A real queue often needs to signal not only "an item arrived" but also "no more items will ever arrive." Channel<T> handles that much more cleanly than most homegrown wrappers.
Summary
- '
ConcurrentQueue<T>is thread-safe, but it does not let consumers await the arrival of new items.' - '
Channel<T>is the modern await-friendly queue abstraction in .NET.' - A custom queue can be built from
ConcurrentQueue<T>plusSemaphoreSlim. - '
BlockingCollection<T>blocks threads rather than supportingawait.' - If your consumer is asynchronous, use a data structure that models signaling as well as storage.

