C# Multithreading with Examples

Learn via video courses
Topics Covered

Overview

Multithreading in C# is a powerful method that allows several threads to run concurrently within a single process. This enables developers to create responsive and efficient programs by effectively ututilizinghe the available CPU cores.

What is Multithreading in C#

Multithreading is one of the most powerful techniques for achieving this in C#. Multithreading refers to a program's ability to run multiple threads at the same time, allowing for parallel processing and increasing overall application efficiency.

Understanding Threads

A thread is a single sequence of instructions or a flow of control within a program in computing. Most applications have traditionally run as single-threaded processes, which means they execute instructions one after the other. However, modern CPUs have multiple cores, and not using them to their full potential means wasting valuable computing power.

This is where multithreading comes in. A program can use multithreading to create and manage multiple threads within a single process. Each thread represents a separate flow of control that can execute its own set of instructions concurrently with other threads, possibly on different CPU cores. Concurrent execution enables tasks to be completed more quickly and efficiently, resulting in improved application performance and responsiveness.

Let us see an example of multithreading in C#:

Output

This is the basic example of creating and working with threads in C# in which we have created a thread and assigned the task to that thread.

Thread priorities in C#

Each thread in the C# Programming Language has a priority, which controls how frequently the thread has access to the CPU. The crucial thing to realize is that how much CPU time a thread receives is determined not only by its priority but also by the type of activity it is conducting. However, it is critical to use thread priorities wisely, as too many high-priority threads can starve lower-priority threads.

Properties of Thread Priority

  1. Lowest = 0: Threads with the lowest priority can be scheduled after threads with higher priorities.
  2. BelowNormal = 1: Threads with BelowNormal priority might be scheduled before Threads with Lowest priority and after Threads with Normal priority.
  3. Normal = 2: Normal priority threads can be scheduled after AboveNormal priority threads and before BelowNormal and Lowest priority threads.
  4. AboveNormal = 3: AboveNormal priority threads can be scheduled after the Highest priority thread and before Normal, BelowNormal, and Lowest priority threads.
  5. Highest = 4: Threads with the highest priority can be scheduled ahead of threads with lower priorities.

How to set the Priority of the Thread

When we create a Thread with no Priority, the default Priority is set to Normal.

Using the Priority property of the Thread class, we can get or set the thread's priority.

  • ThreadPriority Priority {get; set;}: The ThreadPriority property is used to get or set the thread priority. It returns the ThreadPriority values of the thread. The default value is ThreadPriority.Normal.

Now, let's see an example for setting the Priority of the thread:

Example:

Output:

In this example, we first use Thread.CurrentThread to get the priority of the current thread. Before creating and starting the new thread, set the priority and print it to the console. Then, within MyThreadFunction, we retrieve the current thread's priority using the same method to ensure that it is the same as the ThreadPriority.Highest as we set earlier.

Thread Lifecycle

A thread's lifetime refers to the various phases that a thread passes through from the time it is created until it completes its execution or is terminated. Understanding the thread lifecycle is critical for managing threads successfully in a multithreaded application. The thread lifetime in C# has various states:

  1. Unstarted State:
    • Thread status is Unstarted until its Start() method is called.
  2. Runnable State:
    • The thread enters the runnable state after using the Start() function.
    • The thread is not now active on a CPU core, although it is eligible for execution.
  3. Running State:
    • When the thread scheduler assigns the CPU time to the particular thread, it enters the running state.
    • In this state, the thread code is actively executing the task on a CPU core.
  4. Not Runnable State:
    • Threads can enter the not runnable state when they are waiting for some condition to be satisfied before they can continue executing.
    • Once the blocking condition is met, the thread moves back to the runnable state.
  5. Dead State:
    • A thread enters the dead state when its execution is completed or when it is explicitly terminated.
    • Threads can naturally terminate when their method execution completes or when an unhandled exception occurs.
    • Explicitly terminating threads using the Abort() method is discouraged due to potential issues with resource cleanup and thread safety.

The Thread Class in C#

The Thread class in C# is a basic class offered by the .NET Framework for establishing and managing threads. It enables developers to work with multithreading and parallelism, allowing numerous threads to run concurrently within a single process. The Thread class is defined in the System. Threading namespace.

Let's look at some of the Thread class's most important features:

Constructors

The Thread class provides several various constructors to create instances of threads. The commonly used constructors are as follows:

ConstructorsDescription
Thread(ThreadStart, Int32)This constructor creates a new Thread instance by setting the thread's maximum stack size.
Thread(ThreadStart)This constructor creates a new Thread class instance.
Thread(ParameterizedThreadStart, Int32)This constructor creates a new Thread instance by specifying a delegate that permits an object to be provided to the thread when it is started and the maximum stack size for the thread.
Thread(ParameterizedThreadStart)This constructor creates a new Thread instance by specifying a delegate that permits an object to be provided to the thread when it is started.

Properties

The Thread class provides several various properties for the threads. The commonly used properties are as follows:

PropertiesDescription
ThreadStateThis property is used to get the value having the states of the current thread.
PriorityGets or sets the priority of the thread, which affects its preference for CPU time when multiple threads are running.
NameGets or sets the name of the thread, which can help with thread identification during debugging.
ManagedThreadIdThis property is used to get the unique identifier for the currently managed thread.
IsThreadPoolThreadThe value indicating whether a thread is a member of the managed thread pool can be obtained using this property.
IsBackgroundThis property is used to get or set the value indicating whether the thread is a background thread or not.
IsAliveThis property is used to get the value showing the execution status of the current thread.
ExecutionContextThis property is used to get an ExecutionContext object that has information about the various contexts of the current thread.
CurrentUICultureThis property is used to get or set the current culture utilized by the Resource Manager for looking up culture-specific resources at the run time.
CurrentThreadThis property is used to get the currently running thread.
CurrentPrincipalThis property is used to get or set the thread's current principal.
CurrentCultureThis property is used to get or set the culture for the current thread.
CurrentContextThis property is used to get the context in which the thread is executing.
ApartmentStateThis property is used to get or set the apartment state of the particular thread.

Methods

The Thread class provides several various methods for thread execution. The commonly used methods are as follows:

MethodsDescription
Yield()Thread. Yield() allows the current thread to yield its execution to another thread ready to run on the same processor.
VolatileWrite(Byte, Byte)VolatileWrite is a method that provides a memory barrier, ensuring that a write to a volatile field is immediately visible to other threads.
VolatileRead(Byte)VolatileRead is a method that provides a memory barrier, ensuring that a read of a volatile field always fetches the latest value from memory and not from a cached version.
Suspend()This method is used to suspend the Thread
Start()This method changes the state of the current thread instance to Running.
Sleep(Int32)This method pauses the execution of the current thread for the specified number of milliseconds
Resume()This method resumes the suspended thread.
MemoryBarrier()It ensures that all read and write operations before the MemoryBarrier call are completed before any read and write operations that come after it.
Interrupt()This method is used to interrupt a thread that is in the WaitSleepJoin state.
GetType()This method is used to get the Type of the current instance.
GetDomain()This method is used to return the current domain in which the current thread is running.
Finalize()This method is used to ensure that the resources are freed and the other cleanup operations are performed when the garbage collector reclaims the Thread object.
EndThreadAffinity()This method is used to notify the host that the managed code has finished executing instructions.
EndCriticalRegion()This method tells a host that execution is going to reach a region of code where the repercussions of a thread abort or unhandled exception are limited to the current job.
BeginThreadAffinity()This technique alerts a host that managed code is going to execute instructions that are dependent on the identification of the currently active physical operating system thread.
BeginCriticalRegion()This method signals a host when code is entering a critical section where thread aborts or unhandled exceptions might affect other tasks in the application domain.
Abort()To begin the process of terminating the thread, this method throws a ThreadAbortException in the thread on which it is called.

Creating a Thread

For creating a thread using the Thread class, we need to follow these steps:

  1. Define the method that will be executed by the thread. This method's signature must match that of the delegate (ThreadStart or ParameterizedThreadStart).
  2. Create a Thread instance by passing the method as an argument to the appropriate constructor.
  3. Set the thread's name and priority using the Name and Priority properties, if desired.
  4. To begin the thread's execution, use the Start() method.

Here's an example of creating a simple thread using the Thread class:

In this example, we define the MyThreadFunction() method, which will be executed by the new thread. We created a Thread instance called myThread and gave it a name and a priority. We then use the Start() method to start the thread and the Join() method to wait for it to finish before continuing with the main thread's execution.

Advantages of Multithreading

  • Improved Performance: Multithreading allows multiple tasks to be executed concurrently, effectively utilizing multiple CPU cores.
  • Responsiveness: The main thread remains responsive to user interactions by offloading time-consuming operations to separate threads.
  • Resource Utilization: Because threads can handle concurrent operations efficiently, multithreading improves resource utilization.
  • Scalability: Multithreading improves scalability by allowing applications to handle multiple requests at the same time.
  • Background Processing: Multithreading is required for implementing background tasks such as file downloads, data processing, and so on while keeping the UI responsive.

FAQs

Q. Can multithreading lead to issues like race conditions and deadlocks?

A. Yes, multithreading can lead to race conditions, deadlocks, and other synchronization-related issues. When multiple threads access shared resources simultaneously without proper synchronization, race conditions can occur. Deadlocks can happen when threads wait for each other indefinitely, resulting in a program's unresponsiveness.

Q. Can all applications benefit from multithreading?

A. Not all applications can benefit equally from multithreading. The advantages of multithreading are most prominent in scenarios involving CPU-intensive or I/O-bound tasks. Applications with parallelizable workloads and tasks that can execute concurrently are more likely to see significant performance improvements through multithreading.

Q. What are some common pitfalls to avoid when working with multithreading in C#?

A. Common pitfalls to avoid include:

  • Not properly synchronizing shared resources.
  • Using explicit thread manipulation methods like Abort() and Suspend().
  • Creating too many threads, leading to overhead and decreased performance.
  • Not testing multithreaded code thoroughly for race conditions and deadlocks.

Conclusion

  • Multithreading in C# is a powerful method that allows several threads to run concurrently within a single process.
  • Multithreading refers to a program’s ability to run multiple threads at the same time.
  • Multithreading allows for parallel processing and increases overall application efficiency.