C# Threading Gotchas

Introduction

Threading and concurrency is a big topic and there are plenty of resources out there that covers the hows and whats related to starting new threads and avoiding locking up your UI and so on. I will not go into those details but rather try to focus on things that are good to know, but aren’t covered in the normal threading howtos you find online.

When is it worth starting a thread?

The best thread is the one you don’t need. However, a rule of thumb is that operations that might take longer than 50 ms to complete are candidates to run on a separate thread. The reason for that is that there is overhead involved in creating and switching between threads. Also, remember that for I/O bound operations there are often asynchronous methods you can use instead of spawning a thread on your own.

What is the difference between a background and a foreground thread?

The main thread and any thread you create a using System.Threading.Thread is by default a foreground thread. Any task you put on the System.Threading.ThreadPool is run on a background thread.

var tf = new Thread(MyMethod);
tf.Start(); // Starts a new thread that runs in the foreground
...
ThreadPool.QueueUserWorkItem(MyMethod); // Runs on a background thread

There is only one thing that differs between a background and a foreground thread. That is that foreground threads will block the application from exiting until they have completed, but background threads will be abruptly aborted when the application exits. Note: This means that any clean-up actions you have defined, such as removing temporary files, will not be run if they are supposed to happen on a background thread that gets interrupted by the application being shut down. You can however use the Thread.Join method to avoid this problem.

It is also possible to set a new thread to run as background thread if you wish to avoid that it may block application exit. This is a good idea for long running threads that otherwise can lock up the application. Most of us have probably experienced applications becoming unresponsive and the only way to shut them down is via the task manager. This is often caused by hanged foreground threads.

var t = new Thread(MyMethod) { IsBackground = true };
t.Start(); // Runs as a background thread

Also note that all Task-based operations, such as Task.Run, but also await-ed methods, are run on the thread pool, and hence on background threads.

How to catch exceptions on threads?

Take a look at this code sample:

public static void Main()
{
  try
  {
    var t = new Thread(MyMethod);
    t.Start();
  }
  catch (exception ex)
  {
    ...
  }
}

private static void MyMethod { throw null; } // Throws a NullReferenceException

Will the NullReferenceException thrown from MyDelegate be caught? Unfortunately no. Instead the program will terminate due to an unhandled exception.

The reason why the exception cannot be caught this way is simply because each thread has it’s own independent execution path, they progress independently of each other (until they hit a lock or some signaling, like ManualResetEvent). To be able to handle the exception you will have to move the try-catch block into MyMethod:

public static void Main()
{
  var t = new Thread(MyMethod).Start();
}

private static void MyMethod()
{
  try
  {
    throw null;
  }
  catch (exception ex) // Here the exception will be caught
  {
    // Exception handling code. Typically including error logging.
    ... 
  }
}

Note that Tasks, unlike Threads, propagate exceptions. So in the case of using Task.Run you can do this:

public static void Main()
{
  var t = Task.Run(() => { throw null; });
  try
  {
    t.Wait();
  }
  catch (AggregateException ex)
  {
    // Exception handling code.
    // The NullReferenceException is found at ex.InnerException
    ...
  }
}

Tricky captured variables

Consider the following code:

for (var i = 0; i < 10; i++)
{
  var t = new Thread(() => Console.Write(i));
  t.Start();
}

When I ran this I got the following output:

2
3
5
4
5
1
6
9
7
10

Notice how the value 5 is written twice and 0 and 8 is missing, and we actually got the number 10 written. Why does this happen? The answer is that the variable i refers to the same memory during the entire lifetime of the loop. Inside the loop we start 10 different threads that all reads the same memory address when it is about to write the value of i. However, i is updated on the main thread which runs independently of the other 10.

How do you think the code will behave if we make this small change:

for (var i = 0; i < 10; i++)
{
  var temp = i;
  var t = new Thread(() => Console.Write(temp));
  t.Start();
}

This time the numbers 0 to 9 will written without duplicates or missing numbers, the order is still not deterministic though. This is because the line var temp = i creates a new variable for each iteration and copies the current value of i to that location. Each thread will therefore refer to a separate memory location. The threads are however not guaranteed to run in the order they are started.

Ending words

There are lots of things to keep in mind when working with threads. I have touched on some things in this post that I think can be tricky. As usual I recommend having a good book near by that you can use to look up things when they don’t work as you expect.

Rulla till toppen