Back

The Lock Pattern in .NET 9: A Deep Dive Into Thread Synchronization

Oct 25 2024
10min
🕐 Current time : 29 Mar 2025, 05:04 AM
The full Astro logo.

In today’s edition I am going to tackle something that’s both tricky and essential: the lock pattern in .NET 9. If you’ve ever worked on multi-threaded applications, you know how messy things can get when multiple threads try to access shared resources. Let’s dive into how we can handle this gracefully.

The Basics: Why Do We Need Locks?

Imagine this: You’re at a coffee shop, and there’s only one coffee machine. If everyone tried to use it at the same time, you’d end up with a mess of spilled coffee and angry customers. That’s essentially what happens in our code when multiple threads try to access the same resource simultaneously.

Here’s a classic example of what can go wrong without proper locking:

public class BankAccount
{
    private decimal balance = 0;
    
    public void Deposit(decimal amount)
    {
        balance += amount;  // This is not thread-safe!
    }
    
    public void Withdraw(decimal amount)
    {
        balance -= amount;  // This is not thread-safe!
    }
}

Seems innocent, right? But run this with multiple threads, and you might end up with incorrect balances. Yikes!

Enter the Lock Pattern

The lock pattern is like having a “Busy” sign on that coffee machine. Only one person can use it at a time. In C#, we implement this using the lock keyword. Here’s how:

public class ThreadSafeBankAccount
{
    private decimal balance = 0;
    private readonly object lockObject = new object();
    
    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");
        }
    }
}

Advanced Patterns in .NET 9

Now, let’s look at some more sophisticated patterns that .NET 9 brings to the table. One cool feature is the ability to use async locks more efficiently.

The AsyncLock Pattern

public class ModernThreadSafeBankAccount
{
    private decimal balance = 0;
    private readonly AsyncLock asyncLock = new();
    
    public async Task DepositAsync(decimal amount)
    {
        using (await asyncLock.LockAsync())
        {
            await Task.Delay(100); // Simulating some async work
            balance += amount;
        }
    }
    
    // Helper class for async locking
    private class AsyncLock
    {
        private readonly SemaphoreSlim semaphore = new(1, 1);
        
        public async Task<IDisposable> LockAsync()
        {
            await semaphore.WaitAsync();
            return new AsyncLockReleaser(semaphore);
        }
        
        private class AsyncLockReleaser : IDisposable
        {
            private readonly SemaphoreSlim _semaphore;
            
            public AsyncLockReleaser(SemaphoreSlim semaphore)
            {
                _semaphore = semaphore;
            }
            
            public void Dispose()
            {
                _semaphore.Release();
            }
        }
    }
}

Reader-Writer Locks: When You Need More Flexibility

Sometimes you want to allow multiple readers but only one writer. Think of it like a library - many people can read books simultaneously, but only one person should be updating the catalog at a time.

public class ThreadSafeCache<TKey, TValue>
{
    private readonly Dictionary<TKey, TValue> cache = new();
    private readonly ReaderWriterLockSlim lockSlim = new();
    
    public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
    {
        lockSlim.EnterUpgradeableReadLock();
        try
        {
            if (cache.TryGetValue(key, out TValue value))
                return value;
                
            lockSlim.EnterWriteLock();
            try
            {
                if (!cache.ContainsKey(key))
                    cache[key] = valueFactory(key);
                return cache[key];
            }
            finally
            {
                lockSlim.ExitWriteLock();
            }
        }
        finally
        {
            lockSlim.ExitUpgradeableReadLock();
        }
    }
}

Performance Considerations

Let’s talk about some real-world performance implications. I ran some tests on my machine with different locking strategies:

public class LockPerformanceDemo
{
    private const int OperationCount = 1000000;
    private static readonly object simpleLock = new();
    private static readonly ReaderWriterLockSlim rwLock = new();
    
    public async Task RunPerformanceTest()
    {
        var stopwatch = Stopwatch.StartNew();
        
        // Simple lock test
        for (int i = 0; i < OperationCount; i++)
        {
            lock (simpleLock)
            {
                _ = i * 2;
            }
        }
        
        var simpleLockTime = stopwatch.ElapsedMilliseconds;
        stopwatch.Restart();
        
        // Reader-writer lock test
        for (int i = 0; i < OperationCount; i++)
        {
            rwLock.EnterReadLock();
            try
            {
                _ = i * 2;
            }
            finally
            {
                rwLock.ExitReadLock();
            }
        }
        
        var rwLockTime = stopwatch.ElapsedMilliseconds;
        Console.WriteLine($"Simple lock: {simpleLockTime}ms");
        Console.WriteLine($"Reader-writer lock: {rwLockTime}ms");
    }
}

Best Practices and Common Pitfalls

  • Keep Lock Sections Small Don’t do this ever:
lock(lockObject)
{
    // Lots of complex business logic
    // File I/O operations
    // Network calls
    // This is asking for trouble!
}

Do this instead:

var data = PrepareData();  // Outside the lock
lock(lockObject)
{
    // Minimal critical section
    UpdateSharedResource(data);
}
ProcessResult(data);  // Outside the lock
  • Avoid Nested Locks They’re like nested Russian dolls - cute in theory, but a potential deadlock nightmare in practice.
  • Use Private Lock Objects
private readonly object _lockObject = new();  // Good
public readonly object LockObject = new();    // Bad - exposes the lock object

At the End

Locking in .NET 9 gives us powerful tools to handle concurrent access to shared resources. The key is choosing the right pattern for your specific use case:

  • Simple lock for basic synchronization
  • AsyncLock for async scenarios
  • ReaderWriterLockSlim when you need separate read/write access patterns

Remember, with great power comes great responsibility - use these patterns wisely, and your multi-threaded applications will thank you for it!

Feel free to experiment with these patterns in your own code. Just remember to measure performance in your specific scenario, as different patterns can have different impacts depending on your use case.💡

Read more in this Series:

Find me on

GitHub LinkedIn LinkedIn X Twitter
© 2022 to 2025 : Amit Prakash