C# threading allows developers to create multiple threads in C# and .NET. In this article and code example, learn how to use threads in .NET and C# and create your first threading app.
When a new application starts on Windows, it creates a process for the application with a process id, and some resources are allocated to this new process. Every process contains at least one primary thread which takes care of the entry point of the application execution. A single thread can have only one path of execution but as mentioned earlier, sometimes you may need multiple paths of execution, and that is where threads play a role.
Common language runtime
In .NET Core, the common language runtime (CLR) plays a major role in creating and managing threads' lifecycles. In a new .NET Core application, the CLR creates a single foreground thread to execute application code via the Main method. This thread is called the primary thread. Along with this main thread, a process can create one or more threads to execute a portion of the code. A program can also use the ThreadPool class to execute code on worker threads that the CLR manages.
A C# program is single-threaded by design. That means only one path of the code is executed at a time by the primary thread. The entry point of a C# program starts in the Main method, which is the path of the primary thread.
Why threads?
The Main method is the entry point of a C# program. The code in the Main method is executed in a linear fashion in a single, primary thread.
Let's take an example of code in Listing 1.
Listing 1.
In Listing 1, DoSomeHeavyLifting is the first method, which takes about 4 seconds. After that, the DoSomething method is called. In this case, the DoSomething method has to wait until the DoSomeHeavyLifting method is executed. What if there is a function that needs even more extended background work? For example, creating a report and printing it. How about a Windows application with a more extended method, and the user has to wait to do anything else?
Multiple threads or multithreading is the solution for this problem. Multithreading, or simply threading, allows us to create secondary threads that may be used to execute time-consuming background tasks and leave the primary thread available to the main program. This makes an application more responsive and user-friendly.
Now let's replace the DoSomeHeavyLifting method call in the Main method with a new code that creates a new thread. Replace the following code,
With the following code,
Thread.Start() methods start a new thread. This new thread is called a worker thread or a secondary thread. In this code, we have created a new thread object using the Thread class that takes a ThreadStart delegate as a parameter with the method executed in the background.
No matter how long the worker thread method takes, the main thread code will be executed side by side.
The new Main method is listed in Listing 2.
Listing 2.
Now, run the program, and you will see no delay in executing the DoSomething method.
Threads, Resources, and Performance
Remember, creating more threads is not related to processing speed or performance. All lines share the same processor and resources a machine has. In cases of multiple threads, the thread scheduler, with the operating system's help, schedules threads and allocates a time for each thread. But in a single processor machine, only one thread can execute simultaneously. The rest of the threads have to wait until the processor becomes available. If not managed properly, creating more than a few threads on a single processor machine may create a resource bottleneck. Ideally, you want a couple of threads per processor. In the case of dual-core processors, having 4 threads is ideal. In the case of a quad-core processor, you can create up to 8 threads without noticing any issues.
Managing thread resources is also very important for resource-hungry apps. If you've some background IO processing and background database operations, you should manage it so that each thread works with different resources.
Like a process, threads also run within their own boundaries but can communicate with each other, share resources, and pass data among them.
Create and start a thread in C#
The Thread class represents a thread and provides functionality to create and manage a thread's lifecycle and its properties, such as status, priority, and state.
The Thread class is defined in the System.Threading namespace that must be imported before you can use any threading-related types.
The Thread constructor takes a ThreadStart delegate as a parameter and creates a new thread. The parameter of the ThreadStart is the method executed by the new thread. Once a thread is created, it needs to call the Start method to start the thread.
The following code snippet creates a new thread, workerThread, to execute code in the Print method.
The Print method listed below can be used to execute code to do background or foreground work.
Let's try it.
Open Visual Studio. Create a new .NET Core console project. Delete all code and copy and paste (or type) the code in Listing 1.
Listing 1.
The code of Listing 1, the main thread prints 1 to 10 after every 0.2 seconds. The secondary thread prints from 11 to 20 after every 1.0 seconds. We're using the delay for demo purposes, so you can see how two threads execute code in parallel.
The output looks like Figure 1, where the main thread prints a number every 0.2 seconds while the secondary thread prints a number every 1.0 seconds and both threads run in parallel.
Figure 1.
Thread Name, Thread Priority, and Thread State
We can set a thread's name and priority using Name and Priority properties. The following code snippet sets the Name of a thread,
An operating system executes high-priority threads before low-priority threads. The Priority property is used to get and set a thread's priority. The Priority property is of ThreadPriority enum type. ThreadPriority values are Highest, AboveNormal, Normal, BelowNormal, and Lowest. The following code snippet sets a thread priority to the highest.
Thread state
During its lifecycle, each thread goes through a state. The following diagram illustrates various threads states. Thread always states in Unstrated state, and the only transition is to start the thread and state changes to Running. A running thread has three transitions, Suspended, Sleep, and AbortRequested. More states are explained in the table below.
The ThreadState property returns the current state of a thread. You cannot set a thread's state using this property. Table 1 describes various thread states.
State | Description |
A thread is created | Unstarted |
Another thread calls the Thread.Start method on the new thread, and the call returns. During the call to Start, there is no way to know at what point the new thread will start running. The Start method does not return until the new thread has started running. | Running |
The thread calls Sleep | WaitSleepJoin |
The thread calls Wait on another object. | WaitSleepJoin |
The thread calls Join on another thread. | WaitSleepJoin |
Another thread calls Interrupt | Running |
Another thread calls Suspend | SuspendRequested |
The thread responds to a Suspend request. | Suspended |
Another thread calls Resume | Running |
Another thread calls Abort | AbortRequested |
The thread responds to an Abort request. | Stopped |
A thread is terminated. | Stopped |
The thread state includes AbortRequested, which is now dead, but its state has not yet changed to Stopped. | Aborted |
If a thread is a background thread | Background |
Table 1.
A thread can be in more than one state at a time.
The following code snippet checks if a thread state is Running, then aborts it.
Getting the current thread in C#
The Thread.CurrentThread returns the current thread that is executing the current code. The following code snippet prints the current thread's properties, such as its Id, priority, name, and culture,
Thread inherits culture and UI culture from the current system.
Foreground and background threads in C#
There are two types of threads, foreground, and background. Besides the main application thread, all threads created by calling a Thread class constructor are foreground threads.
Background threads are created and used from the ThreadPool, which is a pool of worker threads maintained by the runtime. Background threads are identical to foreground threads with one exception: A background thread does not keep a process running if all foreground threads are terminated. Once all foreground threads have been stopped, the runtime stops all background threads and shuts down.
You can change a thread to execute in the background by setting the IsBackground property anytime. Background threads are useful for any operation that should continue as long as an application runs but should not prevent the application from terminating, such as monitoring file system changes or incoming socket connections.
Pause and abort threads.
Thread.Sleep method can pause the current thread for a fixed period in milliseconds. The following code snippet pauses a thread for 1 second.
The Abort method is used to abort a thread. Make sure you call IsAlive before Abort.
Passing data to a worker thread
It is often required that some data needs to be passed to a worker thread from the main thread. The Thread.Start method has an overloaded form that takes an object type parameter.
The Thread class constructor takes either a ThreadStart or a ParemeterizedThreadStart delegate. The ParemeterizedThreadStart delegate is used when you need to pass to the thread.
Let's look at the following code snippet where the Print class has two methods, PrintJob and PrintPerson. As you can see, both methods take a parameter of the object type. We want to execute these methods in two separate worker threads.
We will now see how to pass an object type from the main thread to a worker thread.
The following code snippet starts a new worker thread that executes the PrintJob method. Since the PrintJob is a ParemeterizedThreadStart delegate, the Start method takes a parameter. In this case, I pass a string as a parameter.
The PrintPerson method of the Print class takes a complex object of type Person. In the following code snippet, I create a Person class and pass it as a Thread parameter.Start method.
The ParameterizedThreadStart delegate supports only a single parameter. You can pass an Array, a collection type, or a tuple type to pass complex or multiple data items.
Thread Pool
Creating and destroying new threads come with a cost that affects an application's performance. Threads can also be blocked or go into sleep or other unresolved states. If your app doesn't distribute workload properly, worker threads may spend most of their time sleeping. This is where the thread pool comes in handy.
A thread pool is a pool of worker threads that have already been created and are available for apps to use as needed. Once thread pool threads finish executing their tasks, they return to the pool.
.NET provides a managed thread pool via the ThreadPool class that the system manages. As developers, we don't need to deal with thread management overhead. For any short background tasks, the managed thread pool is better than creating and managing your own threads. Thread pool threads are suitable only for the background process and are not recommended for foreground threads. There is only one thread pool per process.
Use of thread pool is not recommended when,
- You need to prioritize a thread
- The thread is a foreground thread.
- You have tasks that cause the thread to block for long periods. The thread pool has a maximum number of threads, so a large number of blocked thread pool threads might prevent tasks from starting.
- You need to place threads into a single-threaded apartment. All ThreadPool threads are in the multithreaded apartment.
- You need to have a stable identity associated with the thread or to dedicate a thread to a task.
Learn more about Thread Pool in C# and .NET Core
ThreadPool Thread
The ThreadPool class has several static methods, including the QueueUserWorkItem, which is responsible for calling a thread pool worker thread when it is available. If no worker thread is available in the thread pool, it waits until the thread becomes available.
The QueueWorkItem method takes a procedure that executes in the background.
Here is a complete example of calling a worker thread from the thread poocallinge a method in the background.
You can also pass values to a background method via the QueueWorkItem method. The method's second parameter is an object that can be any object you would like to pass to your background procedure.
Let's assume we have a Person class with the following members.
We can create an object of Person type and pass it to the QueueUserWorkItem method.
And in the worker method, we can extract and use values from the object. In the following example, I read back to the Person.Name, and it displays on the console.
The complete code is listed in Listing 2.
Listing 2.
Maximum and Minimum Thread Pool Threads
Thread pool size is the number of threads available in a thread pool. The thread pool provides new worker threads or I/O completion threads on demand until it reaches the minimum for each category. By default, the minimum number of threads is set to the number of processors on a system. When the minimum is reached, the thread pool can create additional threads in that category or wait until some tasks are complete. The thread pool creates and destroys threads to optimize throughput, defined as the number of tasks completed per unit of time. Too few threads might not make optimal use of available resources, whereas too many threads could increase resource contention.
ThreadPool.GetAvailalbeThreads returns the number of threads that are currently available in a pool. It is the number of maximum threads minus currently active threads.
ThreadPool.GetMaxThreads and ThreadPool.GetMinThreads returns the maximum and minimum threads available in a thread pool.
ThreadPool.SetMaxThreads and ThreadPool.SetMinThreads are used to set a maximum and minimum number of threads on demand as needed in a thread pool. By default, the minimum number of threads is set to the number of processors on a system.
The complete example is listed in Listing 3.
Listing 3.
Summary
This article is an introduction to threading in .NET. In this article, we learned why threading is useful and how to create and manage worker threads to execute background tasks. We also learned about managed thread pools and how to use them to execute background tasks.