Multithreading Overview

Learn what threads are and their importance in responsive applications.

Introduction

Modern applications perform many tasks at the same time. For instance, when we send a photo to our family members with a messenger application, we continue receiving messages while our photo is being uploaded. The method that uploads a photo and the method that receives a message run virtually in a simultaneous manner. Different parts of the system continue to operate at the same time because they run on separate threads. Utilizing several threads is called multithreading.

Note: Threads are unique execution paths with separate control flows. That is, threads don’t affect one another unless they use some common resource, like disk memory.

All code examples we’ve written so far run on a single thread. The subsequent line of code doesn’t run until the previous one finishes executing. In a real-life application, it would look something like this:

  • We click the “Download” button and the download starts. The user interface freezes until the download finishes.

Let’s introduce multithreading to this scenario:

  • We click the “Download” button and the download starts on a background thread. The user interface stays responsive and doesn’t freeze.

Multithread in a multicore environment

Modern computers run on multicore processors, which involves several processors inside a single chip. Each core can handle computations independently. Running several threads on multiple cores would look as follows:

Because the threads run on two different cores, we can assume that they run in parallel.

Multithread on a single core

Multithreading, by default, doesn’t mean parallel execution. It only means that tasks are executed on multiple threads (control flows). Therefore, it’s false to claim that multithreading is only possible on multicore processors. We can have multiple threads running even on a single core. They, however, don’t run at the same time:

In the illustration above, “Thread 1” executes first for some time while “Thread 2” waits. Then, “Thread 2” gets some processor time while “Thread 1” offloads from the core to wait. This is called context switching. The state of an offloaded thread is stored in memory so that it can be restored when it’s loaded to the CPU sometime later.

Multithread in real life

In practice, even if we have a multicore processor, we can’t be sure that our threads run in parallel. That’s because there are many threads running on a modern-day computer. The number of threads is much larger than the number of cores. Therefore, context switches are inevitable.

We care about context switching because it’s a taxing operation. Each execution-context switch introduces some overhead (like copying the state of the current thread to memory and loading the state of the next thread to the CPU).

Threads in .NET

The main .NET thread functionality is located in the System.Threading namespace. This namespace contains the Thread class that represents a single thread in a program. The Thread class defines a number of methods and properties that allow us to control and obtain thread information. For instance, the static CurrentThread property returns the currently running thread.

Note: Even if we don’t explicitly create threads in our .NET apps, there’ll be at least one thread currently executing. That’s the primary thread where our Main method runs.

C#
using System;
using System.Threading;
namespace Threads
{
class Program
{
static void Main(string[] args)
{
// Gets the thread that is currently running
var thread = Thread.CurrentThread;
// Sets the Name property of the Thread instance
thread.Name = "Main thread";
Console.WriteLine($"Name: {thread.Name}.");
}
}
}

The CurrentThread property returns an instance of the Thread class. Here are some of its properties:

  • ManagedThreadId: This returns the thread’s numeric identifier.

  • IsAlive: This indicates whether the thread is currently running.

  • IsBackground: This indicates whether the thread is a background thread.

  • Priority: This stores a ThreadPriority value enum that indicates the priority of the thread.

  • ThreadState: This returns the state of the thread, which is a ThreadState value enum.

Note: There are foreground and background threads. Foreground threads keep the app running. When all foreground threads execute, the app stops. In other words, an application keeps running if at least one foreground thread is active. In contrast, a background thread doesn’t affect application execution. When there are no foreground threads, all background threads stop automatically.

Let’s see these properties inside the code:

C#
using System;
using System.Threading;
namespace Threads
{
class Program
{
static void Main(string[] args)
{
var thread = Thread.CurrentThread;
thread.Name = "Main thread";
Console.WriteLine($"Name: {thread.Name}.");
Console.WriteLine($"Managed thread ID: {thread.ManagedThreadId}.");
Console.WriteLine($"Thread is alive: {thread.IsAlive}.");
Console.WriteLine($"Thread is running in the background: {thread.IsBackground}.");
Console.WriteLine($"Thread priority: {thread.Priority}.");
Console.WriteLine($"Thread state: {thread.ThreadState}.");
}
}
}

Besides properties, the Thread class has several methods that let us control thread execution.