Multithreading with .NET

Introduction

This article explains how multithreading works. You will learn how the operating system manages thread execution and shows you how to manipulate the Thread class in your program to create and start managed threads.
 
This article covers the entire range of threading areas from thread creation, race conditions, deadlocks, monitors, mutexes, synchronization and semaphores and so on.

Multithreading Overview

A thread is an independent stream of instructions in a program. A thread is similar to a sequential program. However, a thread itself is not a program, it can't run on its own, instead it runs within a program's context.

The real usage of a thread is not about a single sequential thread, but rather using multiple threads in a single program. Multiple threads running at the same time and performing various tasks is referred as Multithreading. A thread is considered to be a lightweight process because it runs within the context of a program and takes advantage of resources allocated for that program.
Multithreading1.jpg

With the task manager, you can turn on the Thread column and see the processes and the number of threads for every process. Here, you can notice that only cmd.exe is has a single thread whereas all other applications use multiple threads.

Multithreading2.jpg

The operating system schedules threads. A thread has a priority and every thread has its own stack, but the memory for the program code and heap are shared among all threads of a single process.

A process consists of one or more threads of execution. A process always consists of at least one thread called the primary thread (Main() method in C# programs). A single-threaded process contains only one thread while a multithreaded process contains more than one thread for execution.
Multithreading3.jpg

On a computer, the operating system loads and starts applications. Each application or service runs as a separate process on the machine. The following image illustrates that there are quite a few processes actually running than there are actual applications running in the system. Many of these processes are background operating system processes that are started automatically when the OS loads.

Multithreading4.jpg

System.Threading Namespace

Like many other features, in .NET, System.Threading is the namespace that provides various types to help in construction of multithreaded applications.

TypeDescription
Thread It represents a thread that executes within the CLR. Using this, we can produce additional threads in an application domain.
MutexIt is used for synchronization between application domains.
MonitorIt implements synchronization of objects using Locks and Wait.
SmaphoreIt allows limiting the number of threads that can access a resource concurrently.
InterlockIt provides atomic operations for variables that are shared by multiple threads.
ThreadPool It allows you to interact with the CLR maintained thread pool.
ThreadPriorityThis represents the priority level such as High, Normal, Low.

System.Threading.Thread class

The Thread class allows you to create and manage the execution of managed threads in your program. These threads are called managed threads.

MemberTypeDescription
CurrentThreadStaticReturn a reference of current running thread.
SleepStaticSuspend the current thread for a specific duration.
GetDoaminStaticReturn a reference of current application domain.
CurrentContextStaticReturn a reference of current context in which the thread currently running.
PriorityInstance levelGet or Set the Thread priority level.
IsAliveInstance levelGet the thread state in form of True or False value.
StartInstance levelInstruct the CLR to start the thread.
SuspendInstance levelSuspend the thread.
ResumeInstance levelResume a previously suspended thread.
AbortInstance levelInstruct the CLR to terminate the thread.
NameInstance levelAllows establishing a name to thread.
IsBackgroundInstance levelIndicate whether a thread is running in background or not.

Multithreading Implementation

Obtaining Current Thread Information

To illustrate the basic use of the Thread type, suppose you have a console application in which the CurrentThread property retrieves a Thread object that represents the currently executing thread.

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("**********Current Thread Informations***************\n");

            Thread t = Thread.CurrentThread;

            t.Name = "Primary_Thread";

 

            Console.WriteLine("Thread Name: {0}", t.Name);

            Console.WriteLine("Thread Status: {0}", t.IsAlive);

            Console.WriteLine("Priority: {0}", t.Priority);

            Console.WriteLine("Context ID: {0}", Thread.CurrentContext.ContextID);

            Console.WriteLine("Current application domain: {0}",Thread.GetDomain().FriendlyName);

           

            Console.ReadKey(); 

        }

               

    }

}

After compiling this application, the output will be as in the following;

Multithreading5.jpg

Simple Thread Creation

The following example explains the Thread class implementation in which the constructor of Thread class accepts a delegate parameter. After the Thread class object is created, you can start the thread with the Start() method as in the following;
 

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Thread t = new Thread(myFun);

            t.Start();

 

            Console.WriteLine("Main thread Running");

            Console.ReadKey(); 

        }

 

        static void myFun()

        {

            Console.WriteLine("Running other Thread"); 

        }

    }

}

After running the application, you will get the following output of the two threads:

Multithreading6.jpg

The important point to be noted here is that, there is no guarantee of what output comes first, in other words which thread starts first. Threads are scheduled by the operating system. So which thread comes first can be different each time.

Background Thread

The process of the application keeps running as long as at least one foreground thread is running. If more than one foreground thread is running and the Main() method ends, the process of the application keeps active until all foreground threads finish their work, and by foreground thread termination all background threads will terminate immediately.

When you create a thread with the Thread class, you can define it as being either a foreground or background thread by setting the property IsBackground. The Main() method sets this property of the thread "t" to false. After setting the new thread, the main thread just writes to the console an end message. The new thread writes a start and an end message, and in between it sleeps for 2 seconds.
 

using System;

using System.Threading;

 

namespace threading

{

    class Program

    {

        static void Main(string[] args)

        {

            Thread t = new Thread(myFun);

            t.Name = "Thread1";

            t.IsBackground = false

            t.Start();

            Console.WriteLine("Main thread Running");

            Console.ReadKey(); 

        }

 

        static void myFun()

        {

            Console.WriteLine("Thread {0} started", Thread.CurrentThread.Name);

            Thread.Sleep(2000); 

            Console.WriteLine("Thread {0} completed", Thread.CurrentThread.Name); 

        }

    }

}

When you compile this application, you will still see the completion message written to the console because the new thread is a foreground thread. Here, the output is as in the following;

Multithreading7.jpg

If you change the IsBackground property to true then the result shown at the console will be as in the following;

Multithreading8.jpg

Concurrency issues

When starting multiple threads that access the same data, your program needs to ensure that any piece of shared data is protected against the possibility of numerous threads changing its value.

Race Condition

A race condition occurs if two or more threads access the same object and access to the shared state is not synchronized. To illustrate the problem of a Race condition, let's build a console application. This application uses the Test class to print 10 numbers by pausing the current thread for a random number of times.

Using System;

using System.Threading;

 

namespace threading

{

    public class Test

    {

        public void Calculate()

        {  

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

            Console.WriteLine();

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            Test t = new Test();

            Thread[] tr = new Thread[5];

 

            for (int i = 0; i < 5; i++)

            {

                tr[i] = new Thread(new ThreadStart(t.Calculate));

                tr[i].Name = String.Format("Working Thread: {0}", i);

            }

 

            //Start each thread

            foreach (Thread x in tr)

            {

                x.Start();

            }

            Console.ReadKey();

        }       

    }

}


After compiling this program, the primary thread within this application domain begins by producing five secondary threads. And each working thread is told to call the Calculate() method on the same Test class instance. Hence, all five threads start to access the Calculate() method simultaneously and since we have not taken any precaution to lock down this object's shared resources; this will lead towards the Race Condition and the application produces unpredictable output as in the following;

Multithreading9.jpg

Deadlocks

Having too much locking in an application can get your application into trouble. In a deadlock, at least two threads wait for each other to release a lock. As both threads wait for each other, a deadlock situation occurs and threads wait endlessly and the program stops responding.

Here, both the methods changed the state of objects obj1 and obj2 by locking them. The method DeadLock1() first locks obj1 and next for obj2 similarly method DeadLock2() first locks obj2 and then obj1. So lock for obj1 is released, next a thread switch occurs and the second method starts and gets the lock for obj2. The second thread now waits for the lock of obj1. Both of threads now wait and don't release each other. This is typically a deadlock situation.
 

using System;

using System.Threading;

 

namespace threading

{   

    class Program

    {

        static object obj1 = new object();

        static object obj2 = new object();

 

        public static void DeadLock1()

        {

            lock (obj1)

            {

                Console.WriteLine("Thread 1 got locked");

                Thread.Sleep(500);

               
                lock (obj2)

                {

                    Console.WriteLine("Thread 2 got locked");

                }

            }

        }

 

        public static void DeadLock2()

        {

            lock (obj2)

            {

                Console.WriteLine("Thread 2 got locked");

                Thread.Sleep(500);

               
                lock (obj1)

                {

                    Console.WriteLine("Thread 1 got locked");

                }

            }

        }

 

        static void Main(string[] args)

        {

            Thread t1 = new Thread(new ThreadStart(DeadLock1));

            Thread t2 = new Thread(new ThreadStart(DeadLock2));

 

            t1.Start();

            t2.Start();

 

           Console.ReadKey();

        }

 

       

    }

}

Synchronization

Problems that can occur with multiple threads such as Race condition and deadlocks can be avoided by Synchronization. It is always suggested to avoid concurrency issues by not sharing data between threads. Of course, this is not always possible. If data sharing is unavoidable then you must use synchronization so that only one thread at a time accesses and changes shared states.

This section discusses various synchronization techniques.

Locks

We can synchronize access to shared resources using the lock keyword. By doing so, incoming threads cannot interrupt the current thread, preventing it from finishing its work. The lock keyword requires an object reference.

By taking the previous Race Condition problem, we can refine this program by implementing a lock on crucial statements to make it foolproof from a race conditions as in the following;
 

public class Test

{

    public object tLock = new object();

 

    public void Calculate()

    {

        lock (tLock)

        {

            Console.Write(" {0} is Executing",Thread.CurrentThread.Name);

 

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

            Console.WriteLine();

        }

    }

}

After compiling this program, this time it produced the desired result as in the following. Here, each thread has sufficed opportunity to finish its tasks.

Multithreading10.jpg

Monitor

The lock statement is resolved by the compiler to the use of the Monitor class. The Monitor class is almost similar to a lock but its advantage is better control than the lock statement. You are able to instruct the lock's enter and exit explicitly, as shown in the code below.
 

object tLock = new object();
 

public void Calculate()

{

    Monitor.Enter(tLock);

    try

    {

      for (int i = 0; i < 10; i++)

      {

        Thread.Sleep(new Random().Next(5));

        Console.Write(" {0},", i);

      }

    }

    catch{}

    finally

    {

      Monitor.Exit(tLock);

    }

    Console.WriteLine();

}


In fact, if you observe the IL code of the any application that uses the lock statement, you will find the Monitor class reference there as in the following;

Multithreading11.jpg

Using [Synchronization] Attribute

The [Synchronization] attribute is a member of System.Runtime.Remoting.Context namespace. This class level attribute effectively locks down all instance of the object for thread safety.
 

using System.Threading;

using System.Runtime.Remoting.Contexts; 

 

    [Synchronization]

    public class Test:ContextBoundObject

    {

 

        public void Calculate()

        {

 

            for (int i = 0; i < 10; i++)

            {

                Thread.Sleep(new Random().Next(5));

                Console.Write(" {0},", i);

            }

 

            Console.WriteLine();

        }

    }

Mutex

Mutex stands for Mutual Exclusion that offers synchronization across multiple threads. The Mutex calss is derived from WaitHandle, you can do a WaitOne() to acquire the mutex lock and be the owner of the mutex that time. The mutex is released by invoking the ReleaseMutex() method as in the following;
 

using System;

using System.Threading;

 

namespace threading

{   

    class Program

    {

        private static Mutex mutex = new Mutex();

 

        static void Main(string[] args)

        {

            for (int i = 0; i < 4; i++)

            {

                Thread t = new Thread(new ThreadStart(MutexDemo));

                t.Name = string.Format("Thread {0} :", i+1);

                t.Start();

            }

            Console.ReadKey();

        }

 

        static void MutexDemo()

        {

            try

            {

                mutex.WaitOne();   // Wait until it is safe to enter.

                Console.WriteLine("{0} has entered in the Domain",

                    Thread.CurrentThread.Name);

 

                Thread.Sleep(1000);    // Wait until it is safe to enter.

                Console.WriteLine("{0} is leaving the Domain\r\n",

                    Thread.CurrentThread.Name);

 

            }

            finally

            {

                mutex.ReleaseMutex();

            }

        }

    }

}

Once you have successfully compiled this program, it shows when each new thread first enters into its application domain. Once it has finished its tasks then it is released and the second thread starts and so on.

Multithreading12.jpg

Semaphore

A semaphore is very similar to a Mutex but a semaphore can be used by multiple threads at once while a Mutex can't. With a Semaphore, you can define a count of how many threads are allowed to access the resources shielded by a semaphore simultaneously.

Here in the following example, 5 threads are created and 2 semaphores. In the constructor of the semaphore class, you can define the number of locks that can be acquired with a semaphore.
 

using System;

using System.Threading;

 

namespace threading

{

   

    class Program

    {

        static Semaphore obj = new Semaphore(2, 4);

 

        static void Main(string[] args)

        {

            for (int i = 1; i <= 5; i++)

            {

                new Thread(SempStart).Start(i);

            }

           

            Console.ReadKey();

        }

 

        static void SempStart(object id)

        {

            Console.WriteLine(id + "-->>Wants to Get Enter");

            try

            {

                obj.WaitOne();

                Console.WriteLine(" Success: " + id + " is in!");                         

 

                Thread.Sleep(2000);             

                Console.WriteLine(id + "<<-- is Evacuating");      

            }

            finally

            {

                obj.Release();

               

            }

        }

    }

}

While we run this application, 2 semaphores are immediately created and the others wait because we have created 5 threads. So 3 are in the waiting state. The movement, any one of thread released the rest of created one by one as following.

Multithreading13.jpg

Summary

This article explained how to code multithreaded applications using the System.Threading Namespace. Multithreading in your application can cause concurrency issues such as race conditions and deadlocks.

Finally this article discussed the various techniques for synchronization such as Locks, Mutexes, and Semaphores to handle concurrency problems and protect thread sensitive blocks of code for smooth execution and accurate results.

X

Build smarter apps with Machine Learning, Bots, Cognitive Services - Start free.

Start Learning Now