Multithreading Overview
Understand the fundamentals of multithreading, parallel execution, context switching, and how to interact with the primary thread using modern C#.
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 methods handling the photo upload and incoming messages run almost simultaneously. 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 a shared resource, like memory or file storage.
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.
Multithreading in a multicore environment
Modern computers use multicore processors, which contain several independent processing units within 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.
Multithreading 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, multithreading is not limited to 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.
Multithreading 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 application entry point runs. Modern C# utilizes top-level statements for this entry point, which simplifies the application structure.
Let’s see how to access the primary thread running our application.
Line 1: We import the
System.Threadingnamespace to access thread-related classes.Line 4: We use the
Thread.CurrentThreadproperty to get an instance of the currently running thread.Line 7: We assign a custom string to the
Nameproperty of theThreadinstance to make it easier to identify.Line 8: We print the name of the thread to the console.
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 aThreadPriorityvalue enum that indicates the priority of the thread.ThreadState: This returns the state of the thread, which is aThreadStatevalue 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:
Lines 3–5: We retrieve the current thread, assign it a name, and print the name to the console.
Line 7: We print the
ManagedThreadId, which is a unique numerical identifier assigned to the thread.Line 8: We check the
IsAliveproperty to confirm the thread is actively executing.Line 9: We check the
IsBackgroundproperty to see if the thread runs in the background. Since this is the primary application thread, it returnsFalse.Line 10: We print the thread’s priority level.
Line 11: We print the thread’s current execution state.
Besides properties, the Thread class has several methods that let us control thread execution.