Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

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.QueueUserWorkItem to 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 CountdownEvent to 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 CountdownEvent is always signaled.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.