In C#, threading and synchronization are essential concepts for building applications that can perform multiple tasks concurrently. Threading allows programs to execute multiple operations simultaneously, while synchronization ensures that shared resources are accessed in a thread-safe manner to avoid data corruption or inconsistent states.
A thread is the smallest unit of execution in a program. A single process can have multiple threads running concurrently, allowing multiple operations to be performed at the same time. C# provides the System.Threading namespace, which includes classes and methods to create and manage threads.
In C#, you can create threads using the Thread class. Threads can be started by calling the Start() method, and the code to be executed on the thread is defined in a method or delegate.
using System;
using System.Threading;
public class Program
{
// Method to be executed on a separate thread
static void PrintNumbers()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(i);
Thread.Sleep(1000); // Sleep for 1 second between prints
}
}
public static void Main()
{
// Creating a thread and passing the method to execute
Thread t = new Thread(PrintNumbers);
t.Start(); // Start the thread
Console.WriteLine("Main thread is free to do other work.");
t.Join(); // Wait for the thread to complete
}
}
In this example:
PrintNumbers method.Thread.Start() starts the thread.Thread.Join() waits for the thread to complete before proceeding with the main thread.A thread can be in one of several states during its lifecycle:
The thread pool is a collection of worker threads that can be used for executing tasks asynchronously without manually creating new threads. Using the thread pool is more efficient because it reuses existing threads, minimizing the overhead of thread creation and destruction.
using System;
using System.Threading;
public class Program
{
static void PrintNumbers(object state)
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(i);
Thread.Sleep(1000);
}
}
public static void Main()
{
// Queue a work item to the thread pool
ThreadPool.QueueUserWorkItem(PrintNumbers);
Console.WriteLine("Main thread is free to do other work.");
Thread.Sleep(6000); // Give enough time for the thread pool task to complete
}
}
In this example, ThreadPool.QueueUserWorkItem is used to queue a task for execution on the thread pool.
Synchronization is critical when multiple threads access shared resources to ensure that only one thread can access a resource at a time, preventing data corruption. Without proper synchronization, you can encounter race conditions, where the result depends on the order of thread execution.
One of the primary ways to synchronize threads in C# is by using the lock keyword or the Monitor class. The lock keyword is shorthand for acquiring a lock on an object to ensure that only one thread can execute a block of code at a time.
using System;
using System.Threading;
public class Program
{
static object lockObject = new object();
static int counter = 0;
static void IncrementCounter()
{
lock (lockObject) // Locking the critical section
{
for (int i = 0; i < 1000; i++)
{
counter++; // Critical section
}
}
}
public static void Main()
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Counter value: " + counter); // Expected: 2000
}
}
In this example:
lock keyword ensures that only one thread can enter the IncrementCounter method at a time, preventing a race condition.lockObject is used as the synchronization object, and only one thread can acquire the lock on this object at a time.The Monitor class provides more control over locking. It allows you to explicitly acquire and release locks on an object, and it provides advanced features like timeouts and waiting for conditions.
using System;
using System.Threading;
public class Program
{
static object lockObject = new object();
static void ThreadSafeMethod()
{
Monitor.Enter(lockObject);
try
{
// Critical section
Console.WriteLine("Thread is executing.");
}
finally
{
Monitor.Exit(lockObject);
}
}
public static void Main()
{
Thread t1 = new Thread(ThreadSafeMethod);
Thread t2 = new Thread(ThreadSafeMethod);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
In this example, Monitor.Enter is used to acquire a lock, and Monitor.Exit is used to release it.
A mutex is a synchronization primitive that can be used to manage access to resources across multiple processes, not just threads. It ensures that only one thread or process can enter a critical section at any given time.
using System;
using System.Threading;
public class Program
{
static Mutex mutex = new Mutex();
static void ThreadSafeMethod()
{
mutex.WaitOne(); // Acquire the mutex
try
{
// Critical section
Console.WriteLine("Thread is executing.");
}
finally
{
mutex.ReleaseMutex(); // Release the mutex
}
}
public static void Main()
{
Thread t1 = new Thread(ThreadSafeMethod);
Thread t2 = new Thread(ThreadSafeMethod);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
In this example, mutex.WaitOne() is used to acquire the mutex, and mutex.ReleaseMutex() is used to release it.
A semaphore is used to control access to a resource pool. It allows multiple threads to access a resource concurrently, but only up to a specified limit.
using System;
using System.Threading;
public class Program
{
static Semaphore semaphore = new Semaphore(2, 3); // Max 3 threads can access simultaneously
static void ThreadSafeMethod()
{
semaphore.WaitOne(); // Wait until semaphore is available
try
{
// Simulate work
Console.WriteLine("Thread is executing.");
Thread.Sleep(1000); // Simulate work
}
finally
{
semaphore.Release(); // Release semaphore
}
}
public static void Main()
{
Thread t1 = new Thread(ThreadSafeMethod);
Thread t2 = new Thread(ThreadSafeMethod);
Thread t3 = new Thread(ThreadSafeMethod);
Thread t4 = new Thread(ThreadSafeMethod);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
}
}
In this example:
semaphore.WaitOne() waits until the semaphore is available, and semaphore.Release() releases the semaphore.AutoResetEvent automatically resets after being signaled, whereas a ManualResetEvent remains signaled until explicitly reset.ConcurrentQueue, ConcurrentDictionary, etc., which are optimized for concurrent access.Thread class and thread pools.lock, Monitor, Mutex, Semaphore, and event objects.By understanding and applying these concepts, you can create efficient and reliable multithreaded applications
Open this section to load past papers