In this article, we are going to learn how to run code in another thread in C#. There are many ways to do it in .NET, but we are going to focus on a basic, modern approach.
What Is the Benefit of Running Code in Another Thread?
First of all, why even bother with new threads? There’s a number of reasons, but they all boil down to simple factors – user experience and performance. Offloading some work to a separate thread can work miracles in terms of making the application responsive while it’s busy and in terms of doing many operations at the same time. For example, let’s consider a web browser downloading multiple files at the same time or a music player which allows changing the volume while playing music – none of that would be possible without multithreading.
Details of how .NET and operating systems manage threads, how is that correlated to the hardware, when is it not beneficial to run new threads, how to report progress and many other specifics are definitely out of the scope of this article. Here, let’s focus on a simple practical example – just remember that there are no silver bullets in software development.
Is It Really a ‘New’ Thread?
We are using several terms to describe the same idea – ‘separate thread’, ‘another thread’, ‘new thread’. The idea is to perform an action in a different thread than the one which called it. Let’s keep in mind that there is no guarantee that this will actually create a new thread. .NET utilizes a ThreadPool class which optimizes working with multiple threads by allowing reuse of background threads. Simply speaking, the performance cost of having a number of idle background threads at hand is lower than the cost of creating new ones every time – therefore the framework encourages usage of ThreadPool threads.
Modern Way of Running Code in a New Thread
For a basic example, let’s imagine we have a PDF validator application that checks whether a scanned PDF contains all the necessary stamps and signatures. We want to validate several files at the same time and we want to get the result of the validation of each file as soon as possible – without waiting for all the files to be ready.
Let’s start with the implementation of the (dummy) PDF validator. This is a regular, synchronous code that, in essence, only blocks the current thread for a random number of seconds. It contains some logging logic so that the outcome of the example is simple to understand:
public class PdfValidator { private readonly int _instanceNumber; private static readonly Random _randomizer = new(); public PdfValidator(int instanceNumber) { _instanceNumber = instanceNumber; } internal int Delay { get; set; } = _randomizer.Next(5, 10); public string ValidateFile() { Console.WriteLine($"{ThreadInfo.Log()} PDF Validation {_instanceNumber} starting."); //let's pretend this is a long, CPU-intensive work. Thread.Sleep(TimeSpan.FromSeconds(Delay)); Console.WriteLine($"{ThreadInfo.Log()} PDF Validation {_instanceNumber} finishing after {Delay}s."); //let's get some random dummy result return $"File {_instanceNumber} valid: [{_randomizer.Next(0,2) == 1}]"; } }
In a single-threaded world, we could get the validation result in a console application by a simple invocation:
var result = new PdfValidator(fileNumber).ValidateFile();
Running Code in Separate Threads in a Console Application
Let’s see how it would look if we wanted to run several validations at the same time, in separate threads:
class Program { static async Task Main(string[] args) { Console.WriteLine($"{ThreadInfo.Log()} Starting up PDF validations"); var tasks = new List<Task>(); for (int i = 0; i <= 2; i++) { int instanceNumber = i; //create and start tasks, then add them to the list tasks.Add(Task.Run(() => new PdfValidator(instanceNumber).ValidateFile()).ContinueWith(LogResult)); } Console.WriteLine($"{ThreadInfo.Log()} Now waiting for results..."); //wait until all the tasks in the list are completed await Task.WhenAll(tasks); Console.WriteLine($"{ThreadInfo.Log()} All done."); Console.ReadKey(); } private static void LogResult(Task<string> task) { Console.WriteLine($"{ThreadInfo.Log()} Is Valid: {task.Result}"); } }
We can break down the above snippet into several focus points.
First and foremost, the Task.Run()
invocation. This is a special API for executing operations asynchronously which Microsoft introduced in .NET Framework 4.0. We are passing a lambda expression to it, which specifies the work to be carried out asynchronously: new PdfValidator(instanceNumber).ValidateFile()
.
The Task.Run()
method returns a Task
object, which has a convenient fluent interface that allows chaining another method – ContinueWith()
. This method allows specifying a continuation callback which is executed after finishing the asynchronous task. The ContinueWith()
method returns the same Task
instance.
We are gathering all the Task
objects created by the Task.Run()
invocation to a simple List<Task>
collection, so that later we can wait and resume the work once all of the tasks are finished – regardless of how long each of them took.
This brings us to the next focus point – the Task
type is ‘awaitable’. This means that it allows pausing the execution of the current method until the task that is being awaited (by the use of the await
operator) is finished.
Let’s stress the crucial part – the thread that was executing the code until the await
operator is not blocked. On the contrary, when the execution of the asynchronous method (notice async
in the signature) is suspended on an await
operator, the control is returned to the calling method. The async/await pattern, introduced in C#5.0 works on the basis of low-level events and interrupts, rather than by blocking an idle thread waiting for a background operation to continue. For a deep dive into this, have a look at the classic article by Stephen Cleary.
In the sample code, we are awaiting a special method, await Task.WhenAll(tasks)
which ensures that all the tasks in the list are completed before continuing the execution.
Analyzing the Results of a Multi-Threaded Execution
Let’s analyze how the results of this code could look like. Bear in mind that it might look slightly different each time the code is called because there is no guarantee how long will each operation take, which ThreadPool
thread will be assigned etc.
Thread [1] ThreadPool: False: Starting PDF validations Thread [6] ThreadPool: True: PDF Validation 0 starting. // notice different thread ID Thread [7] ThreadPool: True: PDF Validation 2 starting. // the order presented is different than created Thread [8] ThreadPool: True: PDF Validation 1 starting. // the thread comes from the ThreadPool Thread [1] ThreadPool: False: Now waiting for results... // notice that this call is still on the main thread Thread [7] ThreadPool: True: PDF Validation 2 finishing after 2s. Thread [4] ThreadPool: True: Is Valid: File 2 valid: [True] // each task's result is presented as it is computed Thread [8] ThreadPool: True: PDF Validation 1 finishing after 4s. //without waiting for the rest Thread [8] ThreadPool: True: Is Valid: File 1 valid: [True] Thread [6] ThreadPool: True: PDF Validation 0 finishing after 9s. Thread [6] ThreadPool: True: Is Valid: File 0 valid: [True] Thread [6] ThreadPool: True: All done. // this is displayed after all the tasks are ready
The execution starts in the main thread and the first thing that our code does is to log the “Starting PDF validations” message. Next, it creates new Task
objects in a loop and adds them to the collection. Our demo immediately shows a very important fact – the Tasks
start the execution as soon as they are created rather than when they are awaited. We can also see that all the code that we triggered by the Task.Run()
invocation, including the callbacks, happens on the ThreadPool
thread.
Notice also that the order of the messages about validation starting is in this case indeterminate. This is because the loop executes fast enough to start the tasks almost simultaneously.
Next, we can see that the program outputs the “waiting for results” message, again on the primary thread (ManagedThreadId
= 1). This line is entirely possible to appear immediately after “Starting PDF validations”, depending on which of the simultaneous paths of execution gains access to Console
first, since the Console
class allows only one thread to be producing output at the same time.
As the program runs, it prints out the results of each of the validations as soon as they are available, and only at the very end it displays the final message.
Notice that even though the invocation of the final call happens in the Main
method, the output in the console shows that the current thread is a ThreadPool thread, not the main thread. Let’s see why is that.
Controlling on Which Thread Should the Work Resume
The Task
type allows chaining another method to the invocation – ConfigureAwait(bool continueOnCapturedContext)
. This method allows us to specify the synchronization context on which the code after the await
should be resumed. Simply speaking, it allows the developers to specify explicitly whether they want to continue the work after the await
on the same synchronization context that initiated the await
or not.
Continuation on the captured context is the default option – this is what happens when we do not include the ConfigureAwait()
call. There is a performance overhead related to this default behavior because of the fact that the original synchronization context needs to be available. The creators decided that this default behavior is safer and clearer in the majority of the usages, despite the performance cost. It is, however, a common best practice to always set ConfigureAwait(false)
when it is not required to return to the captured context.
What is this synchronization context, though? Depending on the type of the application, this is an object which allows executing code in the most appropriate place. So, for a WinForms and WPF application, it allows passing delegates to the UI thread, whereas in ASP.NET it is associated with the HTTP request context.
What about console applications?
In this case, there is no special implementation of the synchronization context. The default synchronization context implementation in .NET uses the ThreadPool
, as visible in the program’s output. If that was a UI application, the sample code would resume in the main (UI) thread.
What if the continuation happened on an incorrect SynchronizationContext
? Again, that depends on the type of application – the UI applications would throw an error upon an attempt to access a UI element from a non-UI thread.
Running Tasks in a Separate Thread in a UI Application
The sample application is in WPF, but for simplicity’s sake, let’s ignore things like bindings, view models, progress, continuations, etc.:
private void RunOnMainThreadButton_Click(object sender, RoutedEventArgs e) { this.SingleThreadResultTextBlock.Text = "Started working..."; var result = new PdfValidator().ValidateFile(); //takes several seconds to process this.SingleThreadResultTextBlock.Text = $"Validation result: {result.ResultCode}"; }
This code is the simplest way of handling a button click in an application. Upon a click, it attempts to update the TextBlock with the information that the work has started. Next, it continues to do the actual work and then prints out the result. However, since it all happens on the main thread (the UI thread), it will not work as one might expect.
The UI will not be updated by the time the next operation (ValidateFile()
) starts. Moreover, the UI stays frozen when the UI thread is busy with PDF validation. Only after finishing the execution of that code, the UI thread will get back to working on the UI-related things. It will briefly set the TextBlock text to “StartedWorking…” and then immediately update it with the validation result message.
We can improve it in a simple way – let’s run the validation code asynchronously, in a new thread:
private async void RunOnSeparateThreadButton_Click(object sender, RoutedEventArgs e) { this.MultiThreadResultTextBlock.Text = "Started working..."; var result = await Task.Run(() => new PdfValidator().ValidateFile()); this.MultiThreadResultTextBlock.Text = $"Validation result: {result.ResultCode}"; }
The animation shows the difference between the responsive and freezing behaviors:
What Makes the Application UI Responsible?
Like in the console application, there are a few things to be noticed here: the Task.Run()
instruction and the async
and await
keywords. As we know, Task.Run()
will run the operation on a background thread. This means the PDF validation code will not block the UI thread, but that’s just the first step…
We also want to wait for the validation result, because we want to print out the result in the UI – and this is where async
and await
play their roles. Our UI thread can execute its regular work, i.e. respond to user actions. Only once the background work is done, the ‘awaiting’ ends and the async
method resumes work, moving to the next instruction – update the TextBlock with validation result.
Let’s have a look at the simple diagram showing the flow:
UI Thread ===*event*===\=====able to handle UI events===/===display the result======> Background thread #-------validating pdf---------#
The UI thread is active for the entire lifetime of the application, whereas the background thread is only borrowed to carry out the validation task – after that, the thread is returned to the ThreadPool.
For more details on async
and await
, be sure to see Asynchronous Programming with Async and Await in ASP.NET Core.
Conclusion
In this article, we have covered the simple way of running code in a separate thread. Our examples cover both a console application and an application with UI. We have discussed some of the basic caveats of running code in multiple threads. Finally, we have briefly touched upon the internals of the task-based asynchronous pattern and provided some further readings.
Nice example, but where is “ThreadInfo.Log()” defined?
Hello Paul. It is in a separate file. Feel free to inspect the source code that we share at the beginning of each article: https://github.com/CodeMazeBlog/CodeMazeGuides/blob/main/threads-csharp/MultithreadingInCsharp/RunCodeInNewThreadConsoleApplication/ThreadInfo.cs