C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
170: The lock Statement for Thread Safety
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++;
}
}There are no comments for now.