-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
219: Practice Exercise: Building a Producer-Consumer with BlockingCollection
Up until now, we've talked about the theory of producer-consumer patterns, but the best way to actually "get" BlockingCollection<T> is to see it in action. Instead of a generic example, let's build something I've actually had to implement a dozen times: a basic image processing pipeline. Imagine we have a folder of high-res images that need to be resized. We want one thread scanning the folder (the producer) and another thread doing the heavy lifting of resizing (the consumer).
Setting up the Image Queue
First, I'm going to initialize my BlockingCollection. I'll use strings for the file paths. One thing I always keep in mind: by default, a BlockingCollection is unbounded. For a small exercise, that's fine. In a real production app, I'd probably pass a maximum capacity to the constructor to prevent the producer from eating all the RAM if the consumer can't keep up.
var imageQueue = new BlockingCollection<string>();
Defining the Producer and Consumer
I'll use Task.Run to get these moving on separate threads. The producer is straightforward—it just adds items. The consumer is where the magic happens. I'm using GetConsumingEnumerable() because it's far cleaner than writing a while loop with TryTake. It lets me use a simple foreach loop that blocks automatically when the queue is empty and exits automatically when the collection is marked as complete.
// The Producer: Finds "images" and adds them to the queue
var producer = Task.Run(() => {
string[] files = { "img1.jpg", "img2.jpg", "img3.jpg", "img4.jpg", "img5.jpg" };
foreach (var file in files)
{
Console.WriteLine($"[Producer] Found {file}, adding to queue...");
imageQueue.Add(file);
Thread.Sleep(100); // Simulating some I/O delay
}
});
// The Consumer: Processes the images
var consumer = Task.Run(() => {
foreach (var image in imageQueue.GetConsumingEnumerable())
{
Console.WriteLine($"[Consumer] Processing {image}...");
Thread.Sleep(500); // Simulating heavy CPU work (resizing)
}
Console.WriteLine("[Consumer] All images processed.");
});
The trap I almost fell into
If I ran the code exactly as written above, you'd notice something frustrating: the program would never end. The consumer thread would process the five images and then just... sit there. It's still waiting for more items because it doesn't know the producer is finished. This is a classic mistake when starting with BlockingCollection.
To fix this, the producer has to explicitly signal that it's done. I need to call CompleteAdding(). This tells the GetConsumingEnumerable() iterator that no more items are coming, allowing the foreach loop to finally terminate.
// Fixing the producer logic
var producer = Task.Run(() => {
string[] files = { "img1.jpg", "img2.jpg", "img3.jpg", "img4.jpg", "img5.jpg" };
foreach (var file in files)
{
imageQueue.Add(file);
}
imageQueue.CompleteAdding(); // <--- Crucial call!
Console.WriteLine("[Producer] Finished adding all files.");
});
Wiring it all together
Now, we just need to make sure our main thread waits for both tasks to finish before the application exits. I'll use Task.WaitAll to keep things simple here.
public static void Main()
{
var imageQueue = new BlockingCollection<string>();
var producer = Task.Run(() => {
string[] files = { "img1.jpg", "img2.jpg", "img3.jpg", "img4.jpg", "img5.jpg" };
foreach (var file in files)
{
Console.WriteLine($"[Producer] Adding {file}");
imageQueue.Add(file);
Thread.Sleep(100);
}
imageQueue.CompleteAdding();
Console.WriteLine("[Producer] Done.");
});
var consumer = Task.Run(() => {
foreach (var image in imageQueue.GetConsumingEnumerable())
{
Console.WriteLine($"[Consumer] Resizing {image}...");
Thread.Sleep(500);
}
Console.WriteLine("[Consumer] Done.");
});
Task.WaitAll(producer, consumer);
Console.WriteLine("Pipeline complete.");
}📋 Practical Task
Exercise: Building a Log Processing Pipeline with Bounded Capacity
In this exercise, you will implement a log processing system. To prevent memory issues in a high-traffic system, you must implement a bounded capacity for your collection.
Build a program with the following requirements:
- Create a
BlockingCollection<string>with a maximum capacity of 3 items. - The Producer: Generate 10 log messages (e.g., "Log Entry 1", "Log Entry 2"). Use
Console.WriteLineto show when the producer is attempting to add a log. Because the capacity is only 3, you should notice the producer "pause" once the queue is full until the consumer removes an item. - The Consumer: Process the logs using
GetConsumingEnumerable(). Add aThread.Sleep(1000)inside the consumer loop to simulate a slow write to a database, which will force the bounded capacity to trigger. - Ensure the producer calls
CompleteAdding()and the main thread waits for both tasks to finish.
There are no comments for now.