
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.
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.
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 threadThread newThread = new Thread(PrintNumbers);// Start the threadnewThread.Start();// Execute the same method in the main threadPrintNumbers();}static void PrintNumbers(){for (int i = 1; i <= 10; i++){Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");Thread.Sleep(500);}}}
When you run the “Creating Threads in C#” example, you will see an output like the following:
Thread_1: 1Main: 1Thread_1: 2Main: 2Thread_1: 3Main: 3Thread_1: 4Main: 4Thread_1: 5Main: 5Thread_1: 6Main: 6Thread_1: 7Main: 7Thread_1: 8Main: 8Thread_1: 9Main: 9Thread_1: 10Main: 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.
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 threadThread newThread = new Thread(PrintNumbers);// Start the threadnewThread.Start();// Wait for the thread to completenewThread.Join();// Continue execution in the main threadConsole.WriteLine("Thread has completed.");}static void PrintNumbers(){for (int i = 1; i <= 10; i++){Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");Thread.Sleep(500);}}}
When you run the “Thread Management” example, you will see an output like the following:
Thread_1: 1Thread_1: 2Thread_1: 3Thread_1: 4Thread_1: 5Thread_1: 6Thread_1: 7Thread_1: 8Thread_1: 9Thread_1: 10Thread 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.
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 exampleThread abortThread = new Thread(AbortExample);abortThread.Start();Thread.Sleep(1000);abortThread.Abort(); // Not recommended// Suspend and Resume exampleThread suspendResumeThread = new Thread(SuspendResumeExample);suspendResumeThread.Start();Thread.Sleep(1000);suspendResumeThread.Suspend(); // Not recommendedThread.Sleep(2000);suspendResumeThread.Resume(); // Not recommended// Sleep exampleThread sleepThread = new Thread(SleepExample);sleepThread.Start();// Priority exampleThread 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);}}}
When you run the “Examples”, you will see an output like the following:
AbortExample: 1AbortExample: 2AbortExample: Thread aborted. (Not recommended)SuspendResumeExample: 1SuspendResumeExample: 2SleepExample: Starting...PriorityExample (Lowest): 1PriorityExample (Highest): 1PriorityExample (Highest): 2PriorityExample (Highest): 3PriorityExample (Highest): 4PriorityExample (Highest): 5PriorityExample (Lowest): 2PriorityExample (Lowest): 3PriorityExample (Lowest): 4PriorityExample (Lowest): 5SleepExample: ...FinishedSuspendResumeExample: 3SuspendResumeExample: 4SuspendResumeExample: 5SuspendResumeExample: 6SuspendResumeExample: 7SuspendResumeExample: 8SuspendResumeExample: 9SuspendResumeExample: 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.
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);}}}}
When you run the “Thread Synchronization” example, you will see an output like the following:
Thread_1: 1Thread_1: 2Thread_1: 3Thread_1: 4Thread_1: 5Thread_1: 6Thread_1: 7Thread_1: 8Thread_1: 9Thread_1: 10Thread_2: 1Thread_2: 2Thread_2: 3Thread_2: 4Thread_2: 5Thread_2: 6Thread_2: 7Thread_2: 8Thread_2: 9Thread_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.
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:
ThreadPool
for short-lived tasks to avoid the overhead of creating and destroying threads.async
and await
keywords for I/O-bound operations.Thread.Abort()
and Thread.Suspend()
, as they can lead to unpredictable behavior.Example using the ThreadPool
:
using System;using System.Threading;class Example{static void Main(){// Queue a task on the ThreadPoolThreadPool.QueueUserWorkItem(PrintNumbersWithThreadPool);// Wait for user input before exitingConsole.ReadLine();}static void PrintNumbersWithThreadPool(object state){for (int i = 1; i <= 10; i++){Console.WriteLine($"{Thread.CurrentThread.Name}: {i}");Thread.Sleep(500);}}}
When you run the “Best Practices” example, you will see an output like the following:
Thread_3: 1Thread_3: 2Thread_3: 3Thread_3: 4Thread_3: 5Thread_3: 6Thread_3: 7Thread_3: 8Thread_3: 9Thread_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 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.
Install the following NuGet packages for your test project:
Add a reference to the project containing the threading examples (if not in the same project).
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 outputStringWriter 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 outputConsole.SetOut(new StreamWriter(Console.OpenStandardOutput()));// Parse and validate outputvar 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.
To dive deeper into the topics covered in this post, consider checking out the following resources:
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.
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:
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!
Quick Links
Legal Stuff