HomeContact

Mastering Threading in C# Concepts and Examples

By Shady Nagy
Published in dotnet
April 23, 2023
5 min read
Mastering Threading in C# Concepts and Examples

Table Of Contents

01
Introduction
02
What is Threading?
03
Creating Threads in C#
04
Thread Management
05
Thread Synchronization
06
Best Practices
07
Unit Testing Threading Examples
08
Further Reading
09
Conclusion
10
Feedback and Questions

Introduction

Threading is a powerful technique that allows you to achieve better performance and responsiveness in your applications. In this blog post, we will explore the fundamentals of threading in C# and demonstrate how to create and manage threads using practical examples. By understanding and applying these concepts, you can harness the full potential of your system’s resources and create high-performance applications.

What is Threading?

Threading refers to the concurrent execution of multiple threads within a single process. Threads are lightweight, independent execution units with their own stack, but share the same memory space as the parent process. Threading can help improve the performance of CPU-bound operations by distributing the workload across multiple cores.

Creating Threads in C#

In C#, you can create threads using the System.Threading.Thread class. Here’s an example of how to create and start a new thread:

using System;
using System.Threading;
class Example
{
static void Main()
{
// Create a new thread
Thread newThread = new Thread(PrintNumbers);
// Start the thread
newThread.Start();
// Execute the same method in the main thread
PrintNumbers();
}
static void PrintNumbers()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");
Thread.Sleep(500);
}
}
}

Output Explanation:

When you run the “Creating Threads in C#” example, you will see an output like the following:

Thread_1: 1
Main: 1
Thread_1: 2
Main: 2
Thread_1: 3
Main: 3
Thread_1: 4
Main: 4
Thread_1: 5
Main: 5
Thread_1: 6
Main: 6
Thread_1: 7
Main: 7
Thread_1: 8
Main: 8
Thread_1: 9
Main: 9
Thread_1: 10
Main: 10

This output demonstrates that the PrintNumbers method is executed concurrently by both the main thread and the newly created thread (Thread_1). The two threads interleave their execution, printing numbers from 1 to 10 with a 500ms sleep between each number. Since threads run concurrently, the exact order of the output may vary each time you run the program.

Thread Management

Once you’ve created a thread, you can manage its execution using various methods provided by the Thread class:

  • Start(): Starts the execution of a new thread.
  • Join(): Blocks the calling thread until the thread represented by the current instance terminates.
  • Abort(): Raises a ThreadAbortException to terminate the thread.
  • Suspend(): Suspends the thread.
  • Resume(): Resumes a suspended thread.
  • Sleep(): Blocks the current thread for a specified time.
  • Priority: Sets the scheduling priority of the thread.

Example of starting a thread and using Join() to wait for its completion:

using System;
using System.Threading;
class Example
{
static void Main()
{
// Create a new thread
Thread newThread = new Thread(PrintNumbers);
// Start the thread
newThread.Start();
// Wait for the thread to complete
newThread.Join();
// Continue execution in the main thread
Console.WriteLine("Thread has completed.");
}
static void PrintNumbers()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");
Thread.Sleep(500);
}
}
}

Output Explanation:

When you run the “Thread Management” example, you will see an output like the following:

Thread_1: 1
Thread_1: 2
Thread_1: 3
Thread_1: 4
Thread_1: 5
Thread_1: 6
Thread_1: 7
Thread_1: 8
Thread_1: 9
Thread_1: 10
Thread has completed.

In this example, the PrintNumbers method is executed by the newly created thread (Thread_1). The Join() method is called in the main thread to wait for the new thread to complete its execution. Once the new thread finishes executing the PrintNumbers method, the main thread continues its execution and prints “Thread has completed.”

In this example, we’ll demonstrate the usage of various threading methods available in C#. Each example illustrates a specific method and its effect on the execution of threads. Please note that some methods, such as Abort(), Suspend(), and Resume(), are not recommended for use in modern .NET applications due to their potential to cause unpredictable behavior and deadlocks. Instead, consider using CancellationToken or other synchronization primitives for managing thread execution.

Examples:

Below are examples showcasing the usage of different threading methods, including Abort(), Suspend(), Resume(), Sleep(), and setting thread Priority. The expected results of these examples are also provided.

using System;
using System.Threading;
class ThreadMethodsExample
{
static void Main(string[] args)
{
// Abort example
Thread abortThread = new Thread(AbortExample);
abortThread.Start();
Thread.Sleep(1000);
abortThread.Abort(); // Not recommended
// Suspend and Resume example
Thread suspendResumeThread = new Thread(SuspendResumeExample);
suspendResumeThread.Start();
Thread.Sleep(1000);
suspendResumeThread.Suspend(); // Not recommended
Thread.Sleep(2000);
suspendResumeThread.Resume(); // Not recommended
// Sleep example
Thread sleepThread = new Thread(SleepExample);
sleepThread.Start();
// Priority example
Thread lowPriorityThread = new Thread(PriorityExample) { Priority = ThreadPriority.Lowest };
Thread highPriorityThread = new Thread(PriorityExample) { Priority = ThreadPriority.Highest };
lowPriorityThread.Start();
highPriorityThread.Start();
}
static void AbortExample()
{
try
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"AbortExample: {i}");
Thread.Sleep(500);
}
}
catch (ThreadAbortException)
{
Console.WriteLine("AbortExample: Thread aborted."); // Not recommended
}
}
static void SuspendResumeExample()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"SuspendResumeExample: {i}");
Thread.Sleep(500);
}
}
static void SleepExample()
{
Console.WriteLine("SleepExample: Starting...");
Thread.Sleep(3000);
Console.WriteLine("SleepExample: ...Finished");
}
static void PriorityExample()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"PriorityExample ({Thread.CurrentThread.Priority}): {i}");
Thread.Sleep(1000);
}
}
}

Output Explanation:

When you run the “Examples”, you will see an output like the following:

AbortExample: 1
AbortExample: 2
AbortExample: Thread aborted. (Not recommended)
SuspendResumeExample: 1
SuspendResumeExample: 2
SleepExample: Starting...
PriorityExample (Lowest): 1
PriorityExample (Highest): 1
PriorityExample (Highest): 2
PriorityExample (Highest): 3
PriorityExample (Highest): 4
PriorityExample (Highest): 5
PriorityExample (Lowest): 2
PriorityExample (Lowest): 3
PriorityExample (Lowest): 4
PriorityExample (Lowest): 5
SleepExample: ...Finished
SuspendResumeExample: 3
SuspendResumeExample: 4
SuspendResumeExample: 5
SuspendResumeExample: 6
SuspendResumeExample: 7
SuspendResumeExample: 8
SuspendResumeExample: 9
SuspendResumeExample: 10
  • AbortExample: The thread starts running and prints two lines. After that, the Abort() method is called, which raises a ThreadAbortException. The exception is caught, and the message “Thread aborted.” is printed. Note that the use of Abort() is not recommended.

  • SuspendResumeExample: The thread prints two lines, then the Suspend() method is called, which temporarily pauses the execution of the thread. After a 2-second delay, the Resume() method is called, resuming the execution of the suspended thread. The thread then prints the remaining lines. Note that the use of Suspend() and Resume() is not recommended.

  • SleepExample: The thread starts by printing “SleepExample: Starting…“. It then sleeps (pauses) for 3 seconds using Thread.Sleep(3000), and after the delay, it prints “SleepExample: …Finished”.

  • PriorityExample: Two threads are created with different priorities - one with the lowest priority and the other with the highest priority. Both threads execute the same method but with different scheduling priorities. The highest priority thread gets more CPU time and finishes its execution before the lowest priority thread. This can be observed in the output where the highest priority thread prints all its lines before the lowest priority thread prints the rest of its lines.

Thread Synchronization

When working with multiple threads, it’s crucial to ensure thread safety when accessing shared resources. Synchronization mechanisms like locks, semaphores, and concurrent collections can help protect shared resources and avoid race conditions.

Example using lock:

using System;
using System.Threading;
class Example
{
private static object _lockObject = new object();
static void Main()
{
Thread thread1 = new Thread(PrintNumbersWithLock);
Thread thread2 = new Thread(PrintNumbersWithLock);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
static void PrintNumbersWithLock()
{
lock (_lockObject)
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");
Thread.Sleep(500);
}
}
}
}

Output Explanation:

When you run the “Thread Synchronization” example, you will see an output like the following:

Thread_1: 1
Thread_1: 2
Thread_1: 3
Thread_1: 4
Thread_1: 5
Thread_1: 6
Thread_1: 7
Thread_1: 8
Thread_1: 9
Thread_1: 10
Thread_2: 1
Thread_2: 2
Thread_2: 3
Thread_2: 4
Thread_2: 5
Thread_2: 6
Thread_2: 7
Thread_2: 8
Thread_2: 9
Thread_2: 10

In this example, two threads (Thread_1 and Thread_2) execute the PrintNumbersWithLock method concurrently. The lock statement is used to synchronize access to the shared resource, ensuring that only one thread can execute the code block at a time. This prevents race conditions and ensures that the output of each thread is printed without interleaving.

Best Practices

When working with threads in C#, it’s essential to follow best practices to avoid common pitfalls and ensure the performance and stability of your application:

  • Use the ThreadPool for short-lived tasks to avoid the overhead of creating and destroying threads.
  • Use the async and await keywords for I/O-bound operations.
  • Use the Task Parallel Library (TPL) for CPU-bound operations.
  • Avoid using Thread.Abort() and Thread.Suspend(), as they can lead to unpredictable behavior.
  • Properly handle exceptions in threads to prevent application crashes.
  • Test and profile your application to ensure optimal performance and thread safety.

Example using the ThreadPool:

using System;
using System.Threading;
class Example
{
static void Main()
{
// Queue a task on the ThreadPool
ThreadPool.QueueUserWorkItem(PrintNumbersWithThreadPool);
// Wait for user input before exiting
Console.ReadLine();
}
static void PrintNumbersWithThreadPool(object state)
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");
Thread.Sleep(500);
}
}
}

Output Explanation:

When you run the “Best Practices” example, you will see an output like the following:

Thread_3: 1
Thread_3: 2
Thread_3: 3
Thread_3: 4
Thread_3: 5
Thread_3: 6
Thread_3: 7
Thread_3: 8
Thread_3: 9
Thread_3: 10

In this example, the PrintNumbersWithThreadPool method is executed by a thread from the ThreadPool (Thread_3). The ThreadPool automatically manages the creation and reuse of threads, which can lead to improved performance and resource utilization for short-lived tasks.

Unit Testing Threading Examples

Unit testing is an essential practice for ensuring the correctness and reliability of your code, including multithreaded applications. In this section, we’ll demonstrate how to create unit tests for the threading examples discussed in this post using the popular testing framework, xUnit.

Setting up xUnit

  1. Install the following NuGet packages for your test project:

    • xunit
    • xunit.runner.visualstudio
    • Microsoft.NET.Test.Sdk
  2. Add a reference to the project containing the threading examples (if not in the same project).

Example Unit Test

Let’s create a unit test for the PrintNumbersWithLock method from the “Thread Synchronization” section:

using System;
using System.IO;
using System.Threading;
using Xunit;
public class ThreadingExamplesTests
{
[Fact]
public void TestPrintNumbersWithLock()
{
// Redirect console output
StringWriter sw = new StringWriter();
Console.SetOut(sw);
Thread thread1 = new Thread(Example.PrintNumbersWithLock);
Thread thread2 = new Thread(Example.PrintNumbersWithLock);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
// Reset console output
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()));
// Parse and validate output
var outputLines = sw.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(20, outputLines.Length);
int countThread1 = 0;
int countThread2 = 0;
foreach (var line in outputLines)
{
if (line.StartsWith("Thread_1"))
countThread1++;
else if (line.StartsWith("Thread_2"))
countThread2++;
}
Assert.Equal(10, countThread1);
Assert.Equal(10, countThread2);
}
}

This unit test verifies that the PrintNumbersWithLock method is executed correctly by two separate threads without interleaving, ensuring proper synchronization. The test captures the console output, validates the number of lines printed by each thread, and confirms that each thread printed the expected number of lines.

Unit testing multithreaded code can be challenging due to its non-deterministic nature. However, with careful design and thorough testing, you can ensure the correctness and reliability of your threaded applications.

Further Reading

To dive deeper into the topics covered in this post, consider checking out the following resources:

  1. Threading in C# - A comprehensive guide to threading in C# provided by the official Microsoft documentation.
  2. Task Parallel Library (TPL) - Learn more about the Task Parallel Library, which provides data structures, algorithms, and coordination primitives for parallel and concurrent programming in .NET.
  3. Async and Await - Understand how to use the async and await keywords in C# to write asynchronous code that’s more readable and easier to maintain.
  4. Thread Synchronization in .NET - Explore the various synchronization primitives provided by .NET for managing access to shared resources in multithreaded applications.
  5. ThreadPool - Dive deeper into the ThreadPool class, which efficiently manages the creation and reuse of threads for short-lived tasks.

Conclusion

In this post, we have covered various aspects of threading in C#, including creating and managing threads, thread synchronization, and best practices for working with multithreading in .NET applications. By understanding these concepts and applying the best practices, you can create efficient and thread-safe applications that leverage the full power of modern hardware.

Remember to always test and profile your application, ensuring optimal performance and thread safety. With the right approach and tools, you can build powerful and scalable applications that handle multiple tasks concurrently and efficiently. We hope this post has given you a solid foundation in threading with C# and .NET, and we encourage you to explore the further reading resources to deepen your knowledge in this area.

Feedback and Questions

We’d love to hear your feedback on this tutorial! If you have any questions or suggestions for improvement, please don’t hesitate to reach out. You can leave a comment below, or you can contact us through the following channels:

  1. Email: shady@shadynagy.com
  2. Twitter: @ShadyNagy_
  3. LinkedIn: Shady Nagy
  4. GitHub: ShadyNagy

We’ll do our best to address any questions or concerns you may have. We look forward to hearing from you and helping you make the most of building efficient and reliable multithreaded applications with C# and .NET!


Tags

#.NET7#Threading#Multithreading#Synchronization#Async#Await#ThreadPool#TaskParallelLibrary#CSharp#Microsoft

Share


Previous Article
Building Real-Time Applications with SignalR and .NET 7 A Step-by-Step Guide
Shady Nagy

Shady Nagy

Software Innovation Architect

Topics

AI
Angular
dotnet
GatsbyJS
Github
Linux
MS SQL
Oracle

Related Posts

Getting Started with NDepend A Comprehensive Guide to Boosting Code Quality
Getting Started with NDepend A Comprehensive Guide to Boosting Code Quality
July 03, 2024
4 min

Quick Links

Contact Us

Social Media