C#
Completed
-
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
195: ThreadPool Basics
I've seen a lot of developers jump straight into Task.Run or Parallel.ForEach without ever understanding what's actually happening under the hood. Those are great tools, but they are abstractions. Today, we're going to look at the engine driving them: the ThreadPool.
Think of the ThreadPool as a group of seasoned workers who are already clocked in and standing by. Creating a brand new Thread is expensive—it requires allocating a significant amount of memory for the stack and a lot of overhead from the OS. If you have 1,000 small tasks, creating 1,000 threads will likely crash your app or grind it to a halt. The ThreadPool avoids this by reusing a small number of existing threads to handle a large number of requests.
Simulating a Batch of Log Files
Let's build a simple log analyzer. Imagine we have a directory full of log files, and we want to scan each one for the word "ERROR". Since scanning a file is mostly waiting on I/O and doing light string matching, it's a perfect candidate for the pool.
string[] logFiles = { "log1.txt", "log2.txt", "log3.txt", "log4.txt", "log5.txt" };
void AnalyzeFile(object state)
{
string fileName = (string)state;
Console.WriteLine($"Analyzing {fileName} on thread {Thread.CurrentThread.ManagedThreadId}...");
// Simulate work
Thread.Sleep(new Random().Next(500, 2000));
Console.WriteLine($"Finished {fileName}.");
}
Offloading the Work to the Pool
Now, instead of manually creating a new Thread().Start() for every file, I'll use ThreadPool.QueueUserWorkItem. This tells .NET: "Here is a piece of work; whenever one of your pool threads is free, please run this."
foreach (var file in logFiles)
{
ThreadPool.QueueUserWorkItem(AnalyzeFile, file);
}
Console.WriteLine("All jobs have been queued!");
Wait, where did the output go?
If you run the code exactly as I just wrote it, you'll likely see "All jobs have been queued!" printed to the console, and then the program will immediately exit. You might see one or two "Analyzing..." messages, but most of the work will never finish.
I've made this mistake a dozen times early in my career. I forgot that ThreadPool threads are background threads. Unlike the main thread, background threads don't keep the application alive. As soon as the main thread reaches the end of the Main method, the process terminates, killing all the worker threads instantly, regardless of whether they finished their jobs.
Keeping the Main Thread Alive
To fix this, we need a way to tell the main thread to wait until the workers are done. While we could use a while loop and a counter, that's inefficient (it's called "busy waiting"). A cleaner approach is using a CountdownEvent. This is essentially a thread-safe counter that blocks the calling thread until the count reaches zero.
Here is how I'd rewrite the logic to make it robust:
string[] logFiles = { "log1.txt", "log2.txt", "log3.txt", "log4.txt", "log5.txt" };
// Initialize the counter with the number of tasks we're starting
using CountdownEvent countdown = new CountdownEvent(logFiles.Length);
void AnalyzeFile(object state)
{
try
{
string fileName = (string)state;
Console.WriteLine($"Analyzing {fileName} on thread {Thread.CurrentThread.ManagedThreadId}...");
Thread.Sleep(new Random().Next(500, 2000));
Console.WriteLine($"Finished {fileName}.");
}
finally
{
// Always signal completion in a finally block!
// If the code crashes and we don't signal, the main thread hangs forever.
countdown.Signal();
}
}
foreach (var file in logFiles)
{
ThreadPool.QueueUserWorkItem(AnalyzeFile, file);
}
Console.WriteLine("Jobs queued. Waiting for completion...");
countdown.Wait(); // This blocks the main thread here
Console.WriteLine("All logs analyzed. Exiting.");
Notice that I wrapped the Signal() call in a finally block. In production code, if AnalyzeFile throws an exception and you don't call Signal(), your main thread will wait eternally for a signal that will never come. Always protect your synchronization primitives.
📋 Practical Task
Exercise: Building a Parallel File Checksum Simulator
Your task is to create a program that simulates calculating SHA-256 checksums for a list of 10 large files. Instead of actually reading files from disk, you will simulate the "calculation" using Thread.Sleep.
- Create a list of 10 filenames (e.g., "data1.bin" through "data10.bin").
- Use
ThreadPool.QueueUserWorkItemto process each file. - The worker method should print which thread is handling which file, sleep for a random interval between 200ms and 1000ms, and then print that the checksum is complete.
- Implement a
CountdownEventto ensure the main application does not exit until all 10 simulated checksums are finished. - Ensure that you handle potential exceptions within the worker method so that the
CountdownEventis always signaled.
There are no comments for now.