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.
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());
}
}
}
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.
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());
}
}
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.
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.
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.
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());
}
}
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;
:
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
{
System.Threading.Monitor.Enter(threadSyncObject);
return historyData[historyID];
}
finally
{
System.Threading.Monitor.Exit(threadSyncObject);
}
}
public Object AddHistoryRecord(int historyID, Object historyRecord)
{
try
{
System.Threading.Monitor.Enter(threadSyncObject);
historyData.Add(historyID, historyRecord);
}
finally
{
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:
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
{
System.Threading.Monitor.Enter(threadSync);
return historyData[historyID];
}
finally
{
System.Threading.Monitor.Exit(threadSync);
}
}
public Object AddHistoryRecord(int historyID, Object historyRecord)
{
try
{
System.Threading.Monitor.Enter(threadSync);
historyData.Add(historyID, historyRecord);
}
finally
{
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:
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.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("press any enter to exit");
Console.ReadLine();
}
~Program()
{
System.Threading.Monitor.Enter(this);
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