Book Image

Parallel Programming and Concurrency with C# 10 and .NET 6

By : Alvin Ashcraft
5 (1)
Book Image

Parallel Programming and Concurrency with C# 10 and .NET 6

5 (1)
By: Alvin Ashcraft

Overview of this book

.NET has included managed threading capabilities since the beginning, but early techniques had inherent risks: memory leaks, thread synchronization issues, and deadlocks. This book will help you avoid those pitfalls and leverage the modern constructs available in .NET 6 and C# 10, while providing recommendations on patterns and best practices for parallelism and concurrency. Parallel, concurrent, and asynchronous programming are part of every .NET application today, and it becomes imperative for modern developers to understand how to effectively use these techniques. This book will teach intermediate-level .NET developers how to make their applications faster and more responsive with parallel programming and concurrency in .NET and C# with practical examples. The book starts with the essentials of multi-threaded .NET development and explores how the language and framework constructs have evolved along with .NET. You will later get to grips with the different options available today in .NET 6, followed by insights into best practices, debugging, and unit testing. By the end of this book, you will have a deep understanding of why, when, and how to employ parallelism and concurrency in any .NET application.
Table of Contents (18 chapters)
1
Part 1:Introduction to Threading in .NET
6
Part 2: Parallel Programming and Concurrency with C#
12
Part 3: Advanced Concurrency Concepts

.NET threading basics

It’s time to get started by learning about the basics of threading in C# and .NET. We will be covering the managed threading concepts that are available in .NET 6, but many of these features have been part of .NET since the beginning. The System.Threading namespace has been available since .NET Framework 1.0. In the subsequent 20 years, there have been many useful features added for developers.

In order to responsibly use threading in your applications, you should understand exactly what a thread is and how threads are used by your application’s processes.

Threads and processes

We will start our journey with the basic units of application processing, threads, and processes. A process encapsulates all the execution of an application. This is true for all platforms and frameworks. In .NET, you can think of a process as your .exe or hosted service.

Note

In .NET Framework, the concept of application domains (or app domains), which create isolation units within a process, was introduced. These app domains provide security and reliability by isolating the execution of code loaded into a new app domain. App domains still exist but are not available for developers to create or unload in .NET Core and modern versions of .NET. To read more about app domains, check out this Microsoft Docs article at https://docs.microsoft.com/dotnet/framework/app-domains/application-domains.

A thread represents a single unit of execution within a process. By default, a .NET application will execute all its logic on a single thread (that is, the primary or main thread). Developers can leverage managed threads and other .NET constructs to move from a single-threaded to a multithreaded world, but how do you know when to take this step?

When should we use multithreading in .NET?

There are multiple factors to consider when deciding whether to introduce threading to an application. These factors are both internal and external to the application. The external factors include the hardware in terms of where the application will be deployed, how powerful the processors are where the application will be running, and what other types of processes will be running on these systems?

If your application will be competing for limited resources, it is best to be judicious with the use of multiple threads. If users get the impression that your application is impacting the performance of their systems, you will need to scale back on the number of threads being consumed by your process. Another factor that comes into play is how critical your application is in relation to others on the system. Mission-critical applications will have more resources allocated to remain responsive when needed.

Other common reasons for introducing threading relate to the application itself. Desktop and mobile applications need to keep the user interface (UI) responsive to user input. If the application needs to process a large amount of data or load it from a database, file, or network resource, executing on the main thread can cause the UI to freeze or lag. Also, executing long-running tasks in parallel on multiple threads can reduce the overall execution time of the task.

These operations can be offloaded to background threads if the execution of the tasks is not critical to the application state. Let’s look at the difference between foreground threads and background threads in .NET.

Background threads

The difference between foreground threads and background threads might not be what you think. A managed thread created as a foreground thread is not the UI thread or the main thread. Foreground threads are threads that will prevent the managed process from terminating if they are running. If an application is terminated, any running background threads will be stopped so that the process can shut down.

By default, newly created threads are foreground threads. To create a new background thread, set the Thread.IsBackground property to true before starting the thread. Additionally, you can use the IsBackground property to determine the background status of an existing thread. Let’s look at an example where you might want to use a background thread in your application.

In this example, we will create a console application in Visual Studio that will continuously check the status of a network connection on a background thread. Create a new .NET 6 console app project, name it BackgroundPingConsoleApp, and in Program.cs, enter the following code:

Console.WriteLine("Hello, World!");
var bgThread = new Thread(() =>
{
    while (true)
    {
        bool isNetworkUp = System.Net.NetworkInformation
            .NetworkInterface.GetIsNetworkAvailable();
        Console.WriteLine($"Is network available? Answer: 
            {isNetworkUp}");
        Thread.Sleep(100);
    }
});
bgThread.IsBackground = true;
bgThread.Start();
for (int i = 0; i < 10; i++)
{
    Console.WriteLine("Main thread working...");
    Task.Delay(500);
}
Console.WriteLine("Done");
Console.ReadKey();

Let’s discuss each part of the preceding code before we run it and examine the output:

  1. The first Console.WriteLine statement was created by the project template. We’ll keep this here to help illustrate the order output in the console.
  2. Next, we’re creating a new Thread type named bgThread. Inside the body of the thread, there is a while loop that will execute continuously until the thread is terminated. Inside the loop, we’re calling the GetIsNetworkAvailable method and outputting the result of that call to the console. Before starting over again, we’re using Thread.Sleep to inject a 100-millisecond delay.
  3. The next line after creating the thread is the key part of this lesson:
    bgThread.IsBackground = true;

Setting the IsBackground property to true is what makes our new thread a background thread. This tells our application that the code executing inside the thread is not critical to the application, and the process can terminate without needing to wait for the thread to complete its work. That is a good thing here because the while loop we created will never be complete.

  1. On the next line, we start the thread with the Start method.
  2. Next, the application kicks off some work inside the application’s primary thread. A for loop will execute 10 times and output "Main thread working..." to the console. At the end of each iteration of the loop, Task.Delay is used to wait 500 milliseconds, hopefully providing some time for the background thread to perform some work.
  3. After the for loop, the application will output "Done" to the console and wait for the user input to terminate the application with the Console.ReadKey method.

Now, run the application and examine the console output. You can press any key to stop the application when you feel you have let it run for long enough:

Figure 1.1 – Viewing the threaded console application output

Figure 1.1 – Viewing the threaded console application output

The result might not be what you expected. You can see that the program executed all the logic on the primary thread before starting any of the background thread work. Later, we’ll see how to change the priority of the threads to manipulate which work will be processed first.

What is important to understand, in this example, is that we were able to stop the console application by pressing a key to execute the Console.ReadKey command. Even though the background thread is still running, the process does not consider the thread to be critical to the application. If you comment out the following line, the application will no longer terminate by pressing a key:

bgThread.IsBackground = true;

The application will have to be stopped by closing the command window or using the Debug | Stop Debugging menu item in Visual Studio. Later, in the Scheduling and canceling work section, we will learn how to cancel work in a managed thread.

Before we look at any more examples of using managed threads, we will take some time to learn exactly what they are.

What is managed threading?

In .NET, managed threading is implemented by the System.Threading.Thread class that we used in the previous example. The managed execution environment for the current process monitors all the threads that have been run as part of the process. Unmanaged threading is how threads are managed when programming in C++ with native Win32 threading elements. It is possible for unmanaged threads to enter a managed process through COM interop or through platform invoke (PInvoke) calls from .NET code. If this thread is entering the managed environment for the first time, .NET will create a new Thread object to be managed by the execution environment.

A managed thread can be uniquely identified using the ManagedThreadId property of the Thread object. This property is an integer that is guaranteed to be unique across all threads and will not change over time.

The ThreadState property is a read-only property that provides the current execution state of the Thread object. In the example in the .NET threading basics section, if we had checked the ThreadState property before calling bgThread.Start(), it would have been Unstarted. After calling Start, the state will change to Background. If it were not a background thread, calling Start would change the ThreadState property to Running.

Here is a full list of the ThreadState enum values:

  • Aborted: The thread has been aborted.
  • AbortRequested: An abort has been requested but has not yet been completed.
  • Background: The thread is running in the background (IsBackground has been set to true).
  • Running: The thread is currently running.
  • Stopped: The thread has been stopped.
  • StopRequested: A stop has been requested but has not yet been completed.
  • Suspended: The thread has been suspended.
  • SuspendRequested: Thread suspension has been requested but has not yet been completed.
  • Unstarted: The thread has been created but not yet started.
  • WaitSleepJoin: The thread is currently blocked.

The Thread.IsAlive property is a less specific property that can tell you whether a thread is currently running. It is a boolean property that will return true if the thread has started and has not been stopped or aborted in some way.

Threads also have a Name property that defaults to null if they have never been set. Once a Name property is set on a thread, it cannot be changed. If you attempt to set the Name property of a thread that is not null, it will throw InvalidOperationException.

We will cover additional aspects of managed threads in the remainder of this chapter. In the next section, we will dive deeper into the available methods and options for creating and destroying threads in .NET.