Skip to Content
Course content

170: The lock Statement for Thread Safety

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Concurrency is one of those things that looks perfectly fine in your IDE but falls apart the moment you put it under actual load. I've seen plenty of developers write code that passes every single unit test on their machine, only to have the production database end up with corrupted totals because they didn't account for thread safety. Today, we're talking about the lock statement, which is your first line of defense when multiple threads try to touch the same piece of data at the same time.

Where the numbers stop adding up

Let's look at a scenario I've encountered more times than I'd like to admit: a shared bank account. You might think that simply adding to a balance is a single action, but to the CPU, it's actually three separate steps: read the current value, add the amount, and write the new value back. When two threads do this simultaneously, they can overlap. Thread A reads the balance as $100, then Thread B reads it as $100. Thread A adds $10 and writes $110. Thread B adds $20 and writes $120. Just like that, the $10 from Thread A has vanished into the void.

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        // This looks innocent, but it's a race condition waiting to happen.
        Balance += amount; 
    }
}

If you ran this in a loop with a thousand threads, you'd find that the final balance is almost always lower than it should be. This is a "race condition"β€”the outcome depends on the unpredictable timing of the threads.

Creating a gated entry

To fix this, we need to ensure that only one thread can enter the "critical section" of the code at a time. In C#, we use the lock statement. It acts like a physical key to a room; if one thread has the key, every other thread has to wait in line until the key is handed back. The most important part here is what we lock on. You'll see some people use lock(this), but I strongly advise against that. Locking on this exposes your lock object to the outside world, which can lead to deadlocks if some other piece of code also decides to lock your instance.

Instead, we create a private, dedicated object specifically for synchronization. It doesn't do anything other than act as the "key."

public class BankAccount
{
    private decimal _balance;
    // This object exists solely to be used as a lock
    private readonly object _balanceLock = new object();

    public decimal Balance 
    { 
        get 
        {
            lock (_balanceLock) 
            {
                return _balance;
            }
        } 
    }

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

Now, when Thread B hits the lock statement while Thread A is still updating the balance, Thread B simply pauses. It waits efficiently until Thread A exits the block, at which point Thread B grabs the lock and performs its update using the updated value.

The cost of safety

Now, I won't lie to you: locking isn't free. Every time you use lock, you're introducing a small amount of overhead. More importantly, you're forcing your program to run sequentially in that specific block, which means you're losing some of the performance gains of multi-threading. If you lock too much of your code, you've essentially turned your multi-threaded application back into a single-threaded one.

The trick is to keep your locked sections as small as possible. Don't perform heavy I/O, call long-running API methods, or do complex calculations inside a lock. Get in, update the variable, and get out. If you find yourself locking huge chunks of your logic, that's usually a sign that you need to rethink your data architecture or look into more advanced primitives like ConcurrentDictionary or Interlocked.




πŸ“‹ Practical Task

Fixing the Corrupted Order Counter

You've been handed a OrderProcessor class that is used in a high-traffic web server. The class tracks the total number of orders processed across all threads using a shared counter. However, the logs show that the TotalOrdersProcessed count is consistently lower than the actual number of orders handled.

Your Task: Modify the code below to implement thread safety using the lock statement. Ensure you create a private lock object and protect both the increment logic and the property getter to prevent dirty reads.

public class OrderProcessor
{
    private int _totalOrdersProcessed;

    public int TotalOrdersProcessed
    {
        get { return _totalOrdersProcessed; }
    }

    public void ProcessOrder(int orderId)
    {
        // Simulate some work
        System.Threading.Thread.Sleep(10); 
        
        // TODO: Make this operation thread-safe
        _totalOrdersProcessed++;
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.