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
198: Interlocked Operations
I've seen this exact bug happen in production more times than I care to admit. Usually, it happens when a developer is trying to be "efficient" by avoiding a heavy lock statement for a simple counter. It looks innocent, but it's a ticking time bomb.
public class RequestCounter
{
private int _count = 0;
public void Increment()
{
_count++;
}
public int GetCount() => _count;
}
// Testing it with a few threads
var counter = new RequestCounter();
Parallel.For(0, 10000, i =>
{
counter.Increment();
});
Console.WriteLine($"Expected: 10000, Actual: {counter.GetCount()}");
The Mystery of the Missing Requests
If you run the code above, you'll notice something frustrating: the actual count is almost never 10,000. You might get 9,432 or 8,110. It's non-deterministic; it changes every time you run it.
Here is what's happening under the hood: _count++ looks like a single operation, but it isn't. To the CPU, it's actually three distinct steps:
- Read: Load the current value of
_countfrom memory into a CPU register. - Increment: Add 1 to that register.
- Write: Store the new value back into the memory location.
Imagine two threads hitting that line at the exact same time. Both read the value as 10. Both increment it to 11. Both write 11 back to memory. Two requests happened, but the counter only moved by one. We just "lost" an increment. This is a classic race condition.
Atomic Updates with Interlocked
You could wrap that increment in a lock block, but locks are relatively expensive. They involve the OS kernel and can put threads to sleep, which is overkill for a simple integer. Instead, we use the System.Threading.Interlocked class.
The Interlocked class provides atomic operations. "Atomic" comes from the Greek atomos, meaning "indivisible." An atomic operation happens as a single, uninterrupted unit of work at the hardware level. No other thread can sneak in halfway through the read-modify-write cycle.
public class RequestCounter { private int _count = 0; public void Increment() { // This is now thread-safe and extremely fast Interlocked.Increment(ref _count); } public int GetCount() => _count; }Notice the
refkeyword.Interlockedneeds the actual memory address of the variable to ensure the CPU locks that specific memory bus during the operation.Beyond Simple Addition
Incrementing is the most common use case, but
Interlockedhandles other critical patterns too. For example,Interlocked.Exchangelets you swap a value and get the old one back in one shot. This is incredibly useful for implementing "reset" logic or updating a shared state.Then there is the "big gun":
Interlocked.CompareExchange. This is the foundation of almost all lock-free programming. It says: "If the current value is X, change it to Y. If it's not X, do nothing. Either way, tell me what the value was before I tried."int sharedValue = 0; int expected = 0; int newValue = 10; // If sharedValue == 0, set it to 10. // Returns the original value (which was 0). int original = Interlocked.CompareExchange(ref sharedValue, newValue, expected);I generally recommend using
Interlockedwhenever you're dealing with simple primitive counters or state flags. It's cleaner than a lock and significantly faster because it avoids the overhead of thread synchronization primitives.
📋 Practical Task
Build a Thread-Safe Global Sequence Generator
Imagine you are building a system that assigns a unique sequence number to every outgoing message across a multi-threaded application. You cannot use a lock because the volume of messages is too high and performance is critical.
Your Task:
- Create a class named
SequenceGenerator. - Implement a private integer field to track the current sequence.
- Implement a public method
GetNextSequence()that usesInterlocked.Incrementto return a unique, incrementing number to every caller. - In your
Mainmethod, useParallel.Forto callGetNextSequence()100,000 times. - Store the results in a
ConcurrentBag<int>. - After the loop, verify that the maximum value in the bag is exactly 100,000 and that the bag contains 100,000 unique elements (you can use
.Distinct().Count()for this).
There are no comments for now.