Click here to Skip to main content
15,860,861 members
Articles / Programming Languages / C#
Article

Threads and Synchronization: Part 1

Rate me:
Please Sign up or sign in to vote.
4.68/5 (20 votes)
31 Oct 2007CPOL10 min read 70.2K   90   14
An article related Threads and Thread Synchronization from a Microsoft .NET perspective

Introduction

Today's multi-processing operating system performs multiple operations simultaneously, even if there is only one physical processor in the system. Theoretically it seems impossible, but let's see how multi-processing is achieved with one processor.

There are many stages during the execution of a process where the processor is sitting idle and waiting for some event to occur. For example, if your application is performing some I/O operation, then during I/O operation the processor is sitting idle and waiting for the I/O operation to get completed. Similarly, if your application executes a database query, the processor will be idle and will wait for the database server to respond to the query. Practically speaking, there are numerous stages during the execution of a process when processor is idle. Operating systems utilize this opportunity and employ the idle processor to execute other process. In simple words, the operating system divides the processor time amongst processes to achieve multi-processing.

In the initial days of multi-processing, an operating system used to rely on each process to relinquish the processor regularly to other processes on the system. Thus, in this approach, a poorly designed application or hung application could bring the whole system to a halt. This approach is known as "cooperative multitasking," where each process is supposed to cooperate with the operating system to achieve multiprocessing by surrendering the processor regularly.

The modern operating system follows the "preemptive multitasking" approach, where operating systems have built-in criteria to switch the processor from one process to another and do not depend on underlying processes to relinquish the processor. The act of taking control of the processor from one process and giving it to another process is called "preempting." This approach allows other processes to continue performing, even if one of the processes has hung. The switching of processors is transparent to the processes, so developers do not need to perform any special activity to support multiprocessing operating systems. Criteria for switching the processor from one process to another could be:

  • Elapsed time: Operating system could divide the processor fixed time amongst processes
  • Priority: Higher priority process is waiting for the processor
  • Waiting for an event to occur: Current process is waiting for an I/O and the processor is idle
  • Mixture of the above

The data structure used by the operating system to schedule and divide the processor time amongst processes is known as a "thread." Each process has at least one thread.

So far, we have discussed the execution of multiple processes. Today's technologies, development framework and languages allow execution of multiple tasks within one process simultaneously, e.g. drawing a graph on-screen and calculating a prime number concurrently by the same process. In other words, one process can have more than one operating system thread to execute multiple tasks concurrently.

The following example demonstrates execution of multiple tasks simultaneously by creating a new thread. The newly created thread is used to execute the function ConcurrentTask().The output window shows the results generated by both the main thread and the newly created thread. Note: Each time you execute the following program, you will have a different output, as you cannot predict at what time the operating system will switch the processor between threads.

C#
class Program
    {
        static void Main(string[] args)
        {
            ThreadsExperiment.Program experiment;
            
            experiment = new Program();
            System.Threading.Thread newThread = 
                new System.Threading.Thread(
                new System.Threading.ThreadStart(experiment.ConcurrentTask));
            newThread.Start();
            
            for (int iLoop = 0; iLoop <= 100; iLoop++)
            {
                Console.WriteLine("Main Task " + iLoop.ToString());
            }

            newThread.Join();

            Console.ReadLine();

        }

        public void ConcurrentTask()
        {
            for (int iLoop = 0; iLoop <= 100; iLoop++)
            {
                Console.WriteLine("Concurrent Task " + iLoop.ToString());

            }
        }
  }
Screenshot - image001.jpg

From the above example, multithreading/multitasking seems to be quite an easy and interesting subject. On the basis of my experience, I would strongly suggest using as little thread as possible because controlling the execution of multiple threads within an application is really not an easy game. This is especially the case if threads are accessing the shared resources. In this article, I would discuss synchronization issues with multithreading and various techniques and practices available in Microsoft .NET.

.NET Multithreading

The Microsoft .NET Framework provides various ways of achieving multithreading. The simplest approach is to use the Thread class defined in the System.Threading namespace. Although there is no direct mapping between the operating system thread and the managed thread class, for simplicity the Thread class can be considered as a managed wrapper over the operating system thread. Each managed application can have a default thread provided by the CLR.

You can create your own thread by passing a delegate of type System.Threading.ThreadStart. This delegate references a method that will be executed asynchronously by the newly created thread. .NET 2.0 introduces a new constructor of the Thread class that takes a delegate of type System.Threading.ParameterizedThreadStart as a parameter. This delegate, in turn, takes System.Object as a parameter. Thus, with .NET 2.0 and onwards, you can send any object or even a collection of objects to an asynchronous operation, as defined in the following example.

C#
class Program
    {
        static void Main(string[] args)
        {
            ThreadsExperiment.Program experiment;
            System.Threading.Thread thread;
            int num1;
            int num2;

            System.Threading.Thread.CurrentThread.Name = "Default Thead";
            experiment = new Program();

            Console.Write ("Please enter 1st number: ");
            num1 = int.Parse(Console.ReadLine());

            Console.Write("Please enter 2nd number: ");
            num2 = int.Parse(Console.ReadLine());

            thread = new System.Threading.Thread(new       
            System.Threading.ParameterizedThreadStart(
                experiment.CalculateFactorial));
            thread.Name = "New Thread";
            thread.Start(num1);

            experiment.CalculateFactorial(num2);

            thread.Join();
            Console.ReadLine();
        }

        public void CalculateFactorial(object obj)
        {
            int factorial;

            int num = (int)obj;
            factorial = 1;
            for (int iloop = num; iloop >= 1; iloop--)
            {
                factorial = factorial * iloop;
            }
            Console.WriteLine(System.Threading.Thread.CurrentThread.Name + 
                " calculates the factrorial of " + 
                num.ToString() + " as " + factorial.ToString());
        }
    }
Screenshot - image002.jpg

In the above example, factorials of two numbers are calculated asynchronously. The statement thread.Join() is used to make sure that a newly created thread will have finished processing before terminating the application. Within the .NET Framework, a managed thread can either be a foreground or background thread. CLR assumes that a managed application is running if at least one foreground thread is in the running state. If an application does not have any foreground thread running, CLR will shut the application down. By default, threads created by the Thread class are foreground threads. However, you can set them to background by using the IsBackground property.

.NET Thread Pool

To further facilitate the threading model, Microsoft provides a built-in threading pool in .NET. Creating and destroying threads are resource/time-consuming activities. During the creation of a thread, a kernel thread object is created and initialized. Then the thread stack is allocated and notifications are sent to all the DLLs. Similarly, when a thread is destroyed, a kernel object is freed, stack memory is released and notifications are sent to DLLs.

Thus, to increase performance, CLR provides a pool of threads. When an application wants to perform a task asynchronously, it requests the thread pool to execute that task. The thread pool, by using one of its threads, executes the task. When the task is finished, the thread is returned back to the pool. Thus, for each asynchronous request, a new thread is not created. If an application requests more than the available threads in the pool, the thread pool will either put the request into a waiting queue or will create additional threads. If there are many threads sitting idle in the thread pool, the thread pool will destroy idle threads to release system resources. The following code uses the same example of calculating the factorial of two numbers asynchronously by using the thread pool.

C#
class Program
    {
        static void Main(string[] args)
        {
            ThreadsExperiment.Program experiment;
            int num1;
            int num2;

            experiment = new Program();

            Console.Write ("Please enter 1st number: ");
            num1 = int.Parse(Console.ReadLine());

            Console.Write("Please enter 2nd number: ");
            num2 = int.Parse(Console.ReadLine());

            System.Threading.ThreadPool.QueueUserWorkItem(new 
            System.Threading.WaitCallback(
                experiment.CalculateFactorial), num1);
          
            experiment.CalculateFactorial(num2);
            System.Threading.Thread.Sleep(1000);
            Console.ReadLine();
        }

        public void CalculateFactorial(object obj)
        {
            int factorial;

            int num = (int)obj;
            factorial = 1;
            for (int iloop = num; iloop >= 1; iloop--)
            {
                factorial = factorial * iloop;
            }
            Console.WriteLine(System.Threading.Thread.CurrentThread.Name + 
                " calculates the factrorial of " + 
                num.ToString() + " as " + factorial.ToString());
            
        }
    }

Since all thread pool threads are background threads, the main foreground thread sleeps to make sure that the background threads have completed their tasks. Note: You might have a different result from the below.

Screenshot - image003.jpg

Background Worker

Microsoft .NET also provides the UI component System.ComponentModel.BackgroundWorker to implement multithreading. This UI component internally uses the thread pool to perform tasks asynchronously. This class raises events to let the host know about the status of asynchronous operation. This class is best suited to keeping your UI responsive, even if you are performing some lengthy operation.

Timer

To perform a task asynchronously on an interval basis, Microsoft .NET provides the Timer class. This class internally uses the thread pool to perform tasks asynchronously.

Asynchronous Delegates

When you define a delegate in your application, CLR adds two more methods: BeginInvoke and EndInvoke. These methods are used to asynchronously call the method referenced by the delegate. BeginInvoke returns IAsyncResult, which is used to monitor the progress of asynchronous operation. BeginInvoke internally uses the thread pool class to invoke the method asynchronously.

C#
class Program
    {
        public delegate void FactorialDelegate(object obj);

        static void Main(string[] args)
        {
             ThreadsExperiment.Program experiment;
            FactorialDelegate factorialImpl;
            IAsyncResult asyncResult;
            int num1;
            int num2;

            experiment = new Program();

            Console.Write ("Please enter 1st number: ");
            num1 = int.Parse(Console.ReadLine());

            Console.Write("Please enter 2nd number: ");
            num2 = int.Parse(Console.ReadLine());

            factorialImpl = experiment.CalculateFactorial;
            asyncResult =  factorialImpl.BeginInvoke (num1, null, null);
          
            experiment.CalculateFactorial(num2);

            factorialImpl.EndInvoke(asyncResult);
            Console.ReadLine();
        }
        public void CalculateFactorial(object obj)
        {
            int factorial;

            int num = (int)obj;
            factorial = 1;
            for (int iloop = num; iloop >= 1; iloop--)
            {
                factorial = factorial * iloop;
            }
            Console.WriteLine(System.Threading.Thread.CurrentThread.Name + 
                " calculates the factrorial of " + 
                num.ToString() + " as " + factorial.ToString());
            
        }
    }
Screenshot - image004.jpg

To find out more about delegates, please refer my article Study of Delegates. Until now, I have discussed various ways for developing a multithreaded application in .NET. Following this, I'll discuss some thread synchronization solutions available in the Microsoft .NET Framework.

Thread Synchronization

Thread synchronization is required when two or more threads are accessing a shared resource. Accessing of a shared resource by multiple threads at the same time could destabilize an application; e.g. if two threads are writing in the same file, then data in the file would be unpredictable. A shared resource could be a file, memory block, device or collection of objects.

The segment of code where the thread is accessing the shared resource is known as the "critical section." Thread synchronization solutions ensure that if one thread is executing its critical section, no other thread can enter into the critical section. For writing a stable multithreaded application, one should have a clear understanding of thread synchronization and its solutions. It is one of those programming areas which, if not coded well, could be extremely difficult to debug and could also deteriorate the performance of an application. Thread synchronization solutions must satisfy the following conditions:

  • Mutual exclusion: If a thread is in the critical section, no other thread can enter into the critical section.
  • Progress: If no thread is executing in its critical section and there exist some other threads that wish to enter their critical section, then the selection of the threads that will enter the critical section next cannot be postponed indefinitely.

The Microsoft .NET Framework provides multiple solutions for thread synchronization. We will go through these solutions one-by-one and discuss how they satisfy the above-mentioned requirements for thread synchronization.

SyncBlock

When an object is created in heap, CLR adds one more field known as SyncBlock. CLR provides a mechanism for granting ownership of the SyncBlock of an object to a thread. At any given time, only one thread can own the SyncBlock for a given object. Threads can request ownership of the SyncBlock for a given object obj by executing the statement Monitor.Enter(obj). If no other thread owns the SyncBlock of object obj, then ownership will be granted to the current thread, else the current thread will be suspended. Monitor.Exit(obj) is used to release the ownership. The following example uses the Monitor class to synchronize the threads, accessing a shared resource, historyData;:

C#
class HistoryManager
    {
        private Object threadSyncObject;
        private System.Collections.Hashtable historyData;

        public HistoryManager()
        {
            threadSyncObject = new object();
            historyData = new System.Collections.Hashtable();
        }


        public Object ReadHistoryRecord(int historyID)
        {
            try
            {
                //Try to own SyncBlock of threadSyncObject
                System.Threading.Monitor.Enter(threadSyncObject);
                 return historyData[historyID];
            }
            finally
            {
                System.Threading.Monitor.Exit(threadSyncObject); 
                //Release the ownership
            }
        }

        public Object AddHistoryRecord(int historyID, Object historyRecord)
        {
            try
            {
                //Try to own SyncBlock of threadSyncObject
                System.Threading.Monitor.Enter(threadSyncObject); 
                historyData.Add(historyID, historyRecord);
            }
            finally
            {
                //Release the ownership
                System.Threading.Monitor.Exit(threadSyncObject);   
            }
        }
    }

The C# language provides a simple "lock" statement as an alternative to the Monitor class. When following the SyncBlock approach, please keep in mind the following common mistakes:

Do not use value data types for thread synchronization.

SyncBlock is associated with a reference data type. Thus, if you pass value data types as arguments to Monitor.Enter, they will first be boxed and then ownership of the SyncBlock of the boxed value will be granted to the calling thread. Therefore, if two threads try to acquire ownership for a value type, both will get ownership because both are referring to two different boxed values. Similarly, Monitor.Exit will not release the lock because it will be referring to a different boxed value, as shown in the following example:

C#
class HistoryManager
    {
        private int threadSync;
        private System.Collections.Hashtable historyData;

        public HistoryManager()
        {
            threadSyncObject = new object();
            historyData = new System.Collections.Hashtable();
        }


        public Object ReadHistoryRecord(int historyID)
        {
            try
            {
                //threadSync will be boxed and then 
                //the ownership of SyncBlock of 
                //newly boxed value will be immidiately
                //granted to current thread
                System.Threading.Monitor.Enter(threadSync);   
                return historyData[historyID];
            }
            finally
            {
                //Will not release the lock as this time it 
                //is a different boxed value
                System.Threading.Monitor.Exit(threadSync); 
            }
        }

        public Object AddHistoryRecord(int historyID, Object historyRecord)
        {
            try
            {
                //threadSync will be boxed and then the 
                //ownership of SyncBlock of 
                // newly boxed value will be immidiately
                //granted to current thread
                System.Threading.Monitor.Enter(threadSync);   
                historyData.Add(historyID, historyRecord);
            }
            finally
            {
                //Will not release the lock as this time it 
                //is a different boxed value
                System.Threading.Monitor.Exit(threadSync);    
            }
        }
    }

Do not use the Type class for thread synchronization.

Every type has an associated type descriptor class. You can get the reference of a type descriptor by calling the GetType() method. Since the type descriptor is an object and has an associated SyncBlock, it can be used for thread synchronization. Since there is only one instance per type within a process, you should not use it for thread synchronization, as some unrelated code might have used it for locking purposes and could cause a deadlock. Microsoft recommends using local objects for thread synchronization.

GC collects garbage on its own thread.

If you lock an object and then destroy that object without releasing its lock and if, at the same time, garbage collection starts collecting the object, the finalize method might be called. This is because, when the garbage collector starts collecting the garbage, all other threads are stopped and the finalize method is called by the GC thread. Thus, if you try to lock the object in the finalize method, the GC thread cannot lock it, as it is already locked by the main thread. This will freeze the application. This situation is displayed in the following situation:

C#
class Program
    {
        public delegate void FactorialDelegate(object obj);

        static void Main(string[] args)
        {
            

            ThreadsExperiment.Program experiment;
            experiment = new Program();

            System.Threading.Monitor.Enter(experiment);
            experiment = null;
            
            //GC is forced to collect and call all pending finalize method.
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("press any enter to exit");
            Console.ReadLine();
        }

        ~Program()
        {
            System.Threading.Monitor.Enter(this);
            //Control will never here
            System.Threading.Monitor.Exit(this);
        }
    }

SyncBlock is one of the ways of implementing synchronization among threads in the Microsoft .NET Framework. In Part 2 (which is on its way) of this article, I'll discuss other thread synchronization solutions available in the Microsoft .NET Framework.

History

  • 2 November, 2007 -- Original version posted

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) RBS Bank
United Kingdom United Kingdom
Over 14 years of experience in enterprise / real-time software development using Microsoft Technologies.


More details in LinkedIn Or Blog

Comments and Discussions

 
GeneralMy vote of 4 Pin
extdev11-Mar-11 7:02
extdev11-Mar-11 7:02 
GeneralMy vote of 5 Pin
yanghe16-Dec-10 15:36
yanghe16-Dec-10 15:36 
QuestionBrilliant, thank you :) When is to be released part 2? Pin
irnbru12-Nov-07 1:06
irnbru12-Nov-07 1:06 
AnswerRe: Brilliant, thank you :) When is to be released part 2? Pin
Tariq A Karim12-Nov-07 4:37
Tariq A Karim12-Nov-07 4:37 
GeneralRe: Brilliant, thank you :) When is to be released part 2? Pin
kornman008-Jul-09 23:42
kornman008-Jul-09 23:42 
GeneralExcellent Pin
merlin9817-Nov-07 4:58
professionalmerlin9817-Nov-07 4:58 
GeneralRe: Excellent Pin
Tariq A Karim7-Nov-07 7:27
Tariq A Karim7-Nov-07 7:27 
GeneralRe: Excellent Pin
merlin9817-Nov-07 8:40
professionalmerlin9817-Nov-07 8:40 
GeneralThreading Articles Pin
jpbochi6-Nov-07 2:50
professionaljpbochi6-Nov-07 2:50 
GeneralRe: Threading Articles Pin
jpbochi6-Nov-07 3:10
professionaljpbochi6-Nov-07 3:10 
GeneralRe: Threading Articles Pin
Sire4047-Nov-07 2:03
Sire4047-Nov-07 2:03 
GeneralThreadsExperiment.Program Pin
rctaubert5-Nov-07 14:21
rctaubert5-Nov-07 14:21 
GeneralRe: ThreadsExperiment.Program Pin
Tariq A Karim5-Nov-07 14:51
Tariq A Karim5-Nov-07 14:51 
GeneralRe: ThreadsExperiment.Program Pin
rctaubert5-Nov-07 15:04
rctaubert5-Nov-07 15:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.