How to Use the New Lock Object in C#

By FoxLearn 1/15/2025 2:22:56 AM   62
C# has supported thread synchronization with the `lock` keyword since its early versions, ensuring that only one thread executes a block of code at a time.

In C# 13 and .NET 9, a new `Lock` class was introduced, offering improved thread synchronization with reduced memory usage and faster execution compared to the `lock` statement.

Thread synchronization in C#

Thread synchronization ensures that multiple threads do not access a shared resource at the same time by controlling access to a critical section of code. In C#, this is typically done using the lock keyword, which allows only one thread to execute a block of code, blocking other threads until the lock is released. The critical section refers to the part of code that accesses shared resources.

Using lock in C# guarantees that only one thread can execute a critical section at any given time. When a thread acquires a lock, other threads are blocked until the lock is released.

For example using lock:

private static readonly object _lockObj = new();

public void ProcessOrder()
{
    lock (_lockObj)
    {
        // Code to process the order (critical section)
    }
}

Alternatively, you can use Monitor.Enter and Monitor.Exit to achieve the same result:

private static readonly object _lockObj = new();

public void ProcessOrder()
{
    Monitor.Enter(_lockObj);
    try
    {
        // Code to process the order (critical section)
    }
    finally
    {
        Monitor.Exit(_lockObj);
    }
}

While this approach works, it is generally recommended to use the lock keyword for better readability and safety.

For example, a banking application that synchronizes access to a bank account:

public class BankAccount
{
    private readonly object _lockObject = new();
    private decimal _balance;

    public void Deposit(decimal amount)
    {
        lock (_lockObject)
        {
            _balance += amount;
        }
    }

    public void Withdraw(decimal amount)
    {
        lock (_lockObject)
        {
            if (_balance >= amount)
            {
                _balance -= amount;
            }
            else
            {
                throw new InvalidOperationException("Insufficient funds");
            }
        }
    }

    public decimal Balance => _balance;
}

Introducing the Lock Class in C# 13

In C# 13 and .NET 9, a new System.Threading.Lock class was introduced to improve thread synchronization. This class reduces memory overhead and provides better performance.

For example, how to use the new Lock class:

var lockObj = new System.Threading.Lock();

lock (lockObj)
{
    Console.WriteLine("Executing within the critical section");
}

The code above can be rewritten with the EnterScope method of the Lock class as follows:

Lock.Scope scope = new Lock().EnterScope();
try
{
    Console.WriteLine("Executing within the critical section");
}
finally
{
    scope.Dispose();
}

Using Lock.Scope ensures that the lock is properly released, even if an exception occurs, simplifying resource management.

Using the Lock Class for Synchronization in C# 13

For example, using the Lock class to manage thread synchronization in a file processing application:

public class FileProcessor
{
    private readonly Lock _lockObject = new();
    private int _fileCount = 0;

    public void ProcessFile(string fileName)
    {
        using (_lockObject.EnterScope())
        {
            // Simulate file processing
            _fileCount++;
            Console.WriteLine($"Processing file: {fileName}");
        }
    }

    public int ProcessedFiles => _fileCount;
}

In this case, the Lock.Scope object automatically handles acquiring and releasing the lock, ensuring safe thread synchronization even in the event of errors.

Benchmarking Traditional Locks vs. the New Lock Class

To compare the performance of the traditional lock keyword with the new Lock class, you can use the BenchmarkDotNet library.

After installing it via NuGet, you can update the FileProcessor class to include both locking methods for benchmarking:

public class FileProcessor
{
    private readonly Lock _lockObjectNewApproach = new();
    private readonly object _lockObjectTraditionalApproach = new();
    private int _fileCountTraditional = 0;
    private int _fileCountNew = 0;

    public void ProcessFileTraditional(string fileName)
    {
        lock (_lockObjectTraditionalApproach)
        {
            _fileCountTraditional++;
            Console.WriteLine($"Processing file: {fileName}");
        }
    }

    public void ProcessFileNew(string fileName)
    {
        using (_lockObjectNewApproach.EnterScope())
        {
            _fileCountNew++;
            Console.WriteLine($"Processing file: {fileName}");
        }
    }
}

Next, create a benchmark class to compare both methods:

[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class LockPerformanceBenchmark
{
    [Params(10, 100, 1000, 10000)]
    public int N;

    [Benchmark]
    public void ProcessFileTraditional()
    {
        FileProcessor processor = new FileProcessor();
        for (int i = 0; i < N; i++)
        {
            processor.ProcessFileTraditional($"File_{i}");
        }
    }

    [Benchmark]
    public void ProcessFileNew()
    {
        FileProcessor processor = new FileProcessor();
        for (int i = 0; i < N; i++)
        {
            processor.ProcessFileNew($"File_{i}");
        }
    }
}

To run the benchmark, use the following code:

using BenchmarkDotNet.Running;
var summary = BenchmarkRunner.Run<LockPerformanceBenchmark>();

Once compiled and executed, the benchmarks will show that the new Lock class typically performs better than the traditional lock keyword.

The Lock class in C# 13 offers enhanced thread synchronization with better performance and reduced memory overhead compared to the traditional lock keyword. While using locks can complicate code and introduce risks like deadlocks, the new Lock class simplifies synchronization management and is generally the preferred method for thread synchronization in modern C# applications.