Thread Synchronization with Advanced Lock in C#

Thread Synchronization with Advanced Lock in C#

In this article, I will discuss Thread Synchronization with Advanced Lock in C# with Examples. Please read our previous articles discussing Enhanced Collection Support With Params Modifier in C# with Examples.  C# 13 introduces a new thread synchronization mechanism with System.Threading.Lock. This feature enhances traditional synchronization techniques by offering better performance, flexibility, and structure when working with multi-threaded applications. It improves how locks are handled in C#, making it easier to manage thread synchronization across complex multi-threaded systems.

What is Lock in C#?

In C#, a lock prevents multiple threads from accessing shared resources concurrently. It ensures that only one thread can access a critical code section at a time. Locks are crucial in multi-threaded programming because they avoid data corruption that could occur if multiple threads modify the same data simultaneously. C# provides several ways to synchronize access to shared resources:

  • Monitor: This is the underlying mechanism behind the lock keyword in C#.
  • Mutex: Used for inter-process synchronization.
  • Semaphore: Controls access to a resource pool.

The lock keyword in C# is essentially a shorthand for using Monitor.Enter and Monitor.Exit to acquire and release a lock.

How Lock is Working Before C# 13?

Before C# 13, thread synchronization using the lock keyword worked as follows:

  • A thread enters a critical section by acquiring a lock on an object. The lock is typically placed around the shared resource to protect it from concurrent access.
  • The lock keyword uses Monitor.Enter to acquire the lock and Monitor.Exit to release it when the code inside the locked section finishes executing.
  • If another thread tries to enter the critical section, it will be blocked until the lock is released.

The following example demonstrates thread synchronization before C# 13:

namespace CSharp13NewFeatures
{
    public class Program
    {
        // Shared resource (counter)
        static int counter = 0;

        // Lock object
        static readonly object lockObj = new object();

        static void Main(string[] args)
        {
            // Create multiple threads
            Thread thread1 = new Thread(IncrementCounter);
            Thread thread2 = new Thread(IncrementCounter);

            // Start threads
            thread1.Start();
            thread2.Start();

            // Wait for threads to complete
            thread1.Join();
            thread2.Join();

            // Final counter value after both threads execute
            Console.WriteLine($"Final Counter Value: {counter}");
        }

        // Method to increment the counter
        static void IncrementCounter()
        {
            // Locking the critical section
            lock (lockObj)
            {
                int temp = counter;
                temp++;
                Thread.Sleep(10);  // Simulating some work
                counter = temp;
                Console.WriteLine($"Counter: {counter}");
            }
        }
    }
}
Explanation of the Code:
  • Shared Resource: The counter variable is a shared resource accessed and modified by multiple threads.
  • Locking: The lock (lockObj) statement ensures that only one thread can increment the counter at a time, preventing race conditions.
  • Threading: Two threads (thread1 and thread2) are created and started. Both threads attempt to increment the counter.
  • Thread Safety: The lock ensures thread safety by allowing only one thread to enter the critical section at a time.
Output:

How Lock is Working Before C# 13?

How Lock Works Before C# 13:
  • When both threads attempt to access the critical section, the lock ensures that only one thread can execute the code inside the lock block at a time.
  • Before proceeding, the second thread will have to wait until the first thread releases the lock (by exiting the critical section).
What is the Advanced Thread Synchronization with the System.Threading.Lock in C# 13?

Starting in .NET 9 / C# 13, a new System.Threading.Lock type has been introduced, providing an improved way to handle thread synchronization. This new feature enhances thread synchronization by offering lower overhead, better exception safety, and more efficient scoping than traditional lock and Monitor.Enter/Exit.

How System.Threading.Lock works:
  • Detecting the Lock Type: Instead of using a plain object (e.g., lock (myLock)), you can use a Lock instance (e.g., lock (myLock) where myLock is an instance of System.Threading.Lock).
  • Behind the Scenes: The new synchronization API uses Lock.EnterScope() and Dispose() instead of Monitor.Enter/Exit. EnterScope() returns a ref struct, which ensures that the lock is automatically released when the block exits, even if an exception occurs.
Example to Understand the New Thread Synchronization in C# 13

Now, we will rewrite the previous example to use the new System.Threading.Lock. Please modify the program class as follows. The following code is self-explained, so please read the comment lines for a better understanding.

namespace CSharp13NewFeatures
{
    class Program
    {
        // Shared resource (counter)
        static int counter = 0;

        // Lock object using the new System.Threading.Lock
        static readonly System.Threading.Lock lockObj = new System.Threading.Lock();

        // Method to increment the counter
        static void IncrementCounter()
        {
            // Locking the critical section using the new Lock
            using (lockObj.EnterScope())  // EnterScope acquires the lock
            {
                int temp = counter;
                temp++;
                Thread.Sleep(10);  // Simulating some work
                counter = temp;
                Console.WriteLine($"Counter: {counter}");
            }
        }

        static void Main(string[] args)
        {
            // Create multiple threads
            Thread thread1 = new Thread(IncrementCounter);
            Thread thread2 = new Thread(IncrementCounter);

            // Start threads
            thread1.Start();
            thread2.Start();

            // Wait for threads to complete
            thread1.Join();
            thread2.Join();

            // Final counter value after both threads executes
            Console.WriteLine($"Final Counter Value: {counter}");
        }
    }
}
Explanation of the Code:
  • New Lock Type: lockObj is now an instance of System.Threading.Lock instead of a simple object. We use lockObj.EnterScope() to acquire the lock ensures that only one thread can enter the critical section at a time.
  • Acquiring and Releasing the Lock: The EnterScope() method acquires the lock and ensures it is released automatically when the using block is exited. When the code exits the using block, Dispose() is automatically called, releasing the lock and ensuring that other threads can access the critical section.
  • Improved Performance: The new System.Threading.Lock type simplifies thread synchronization by automatically handling the lock acquisition and release. This reduces the explicitly call Monitor.Enter and Monitor.Exit. The internal management of Lock results in lower overhead compared to traditional locking mechanisms, providing better performance in scenarios with high concurrency (many threads accessing shared resources).
  • Thread Synchronization: We use the lock mechanism to ensure that only one thread increments the counter at a time. This prevents race conditions that could result in incorrect counter values.
Output:

Advanced Thread Synchronization with the System.Threading.Lock in C# 13

Key Benefits of Using the System.Threading.Lock:
  • Automatic Lock Release: By using EnterScope(), the lock is automatically released when the code exits the scope, eliminating the need for explicit Monitor.Exit() or lock statement.
  • Cleaner Scoping: Using using (_sync.EnterScope()) ensures that the lock is held only for the scope of the block, and the lock is released automatically once the block ends.
  • Improved Exception Handling: Even if an exception occurs within the locked section, the lock will be automatically released when exiting the scope.
Real-Time Scenario and Use Case

Let’s consider a real-time scenario with a high-concurrency environment, such as a banking system, where multiple threads update a user’s balance simultaneously. We must ensure thread synchronization to prevent race conditions where two threads might attempt to update the balance simultaneously, resulting in incorrect values. In such a case, using System.Threading.Lock ensures that only one thread can access and update the balance simultaneously, preventing inconsistent state.

namespace CSharp13NewFeatures
{
    public class Program
    {
        // Shared resource (account balance)
        private static decimal _accountBalance = 1000.0m;

        // Lock object using System.Threading.Lock
        private static readonly Lock _balanceLock = new Lock();

        // Method to simulate a deposit operation
        static void Deposit(decimal amount)
        {
            using (_balanceLock.EnterScope())
            {
                decimal tempBalance = _accountBalance;
                tempBalance += amount;  // Simulate a deposit
                Thread.Sleep(10);  // Simulating work
                _accountBalance = tempBalance;  // Update account balance
                Console.WriteLine($"Deposited {amount}. Current balance: {_accountBalance}");
            }
        }

        static void Main()
        {
            // Simulating multiple threads performing deposits
            Thread[] threads = new Thread[5];
            for (int i = 0; i < threads.Length; i++)
            {
                threads[i] = new Thread(() => Deposit(100.0m));
                threads[i].Start();
            }

            foreach (var thread in threads)
            {
                thread.Join();  // Wait for all threads to finish
            }

            Console.WriteLine($"Final account balance: {_accountBalance}");
        }
    }
}
Code Explanation:
  • Shared Resource (_accountBalance): This represents the bank account balance, which is accessed and modified by multiple threads.
  • Synchronization with System.Threading.Lock: The using (_balanceLock.EnterScope()) ensures that only one thread can perform the deposit operation at a time, preventing race conditions.
  • Simulating Work: We simulate some work (depositing an amount) and simulate the delay with Thread.Sleep(10).
  • Final Balance: After all threads complete their execution, the final account balance is displayed, ensuring the operations are synchronized correctly.
Output:

Thread Synchronization with Advanced Lock in C# with Examples

The new System.Threading.Lock feature in C# 13 provides a cleaner, more efficient way to handle thread synchronization. It reduces the overhead compared to traditional lock usage, and EnterScope guarantees that the lock is automatically released, even in the presence of exceptions, ensuring thread safety and cleaner code.

In high-concurrency scenarios, such as banking systems or real-time processing systems, where multiple threads interact with shared resources, this new feature ensures data integrity without needing to manage locking or deal with potential exceptions manually.

In the next article, I will discuss Partial Properties and Indexers in C# with Examples. I explain Thread Synchronization with Advanced Lock in C# in this article with Examples. I would like to have your feedback. Please post your feedback, questions, or comments about this article.

Leave a Reply

Your email address will not be published. Required fields are marked *