lock and SemaphoreSlim
Synchronization primitives protect shared resources from concurrent access. lock provides mutual exclusion, while SemaphoreSlim limits the number of concurrent accesses.
The lock Statement
lock ensures only one thread can execute a code block at a time:
csharp1private readonly object _lockObj = new();2private int _counter = 0;34void IncrementSafely()5{6 lock (_lockObj)7 {8 _counter++; // Only one thread at a time9 }10}
How lock Works
1Thread 1 Thread 22-------- --------3Acquire lock4Enter critical section5 Try to acquire lock6 BLOCKED (waiting)7_counter++8Exit critical section9Release lock10 Acquire lock11 Enter critical section12 _counter++13 Exit critical section14 Release lock
Lock Object Best Practices
csharp1// GOOD: Private, readonly, dedicated lock object2private readonly object _lock = new();34// BAD: Locking on 'this'5lock (this) // External code could lock on same object!67// BAD: Locking on a Type8lock (typeof(MyClass)) // Type objects are shared!910// BAD: Locking on strings11lock ("my lock") // String interning makes this shared!1213// BAD: Locking on value types14lock (myInt) // Boxing creates new object each time - no protection!
Complete Example
csharp1class ThreadSafeCounter2{3 private readonly object _lock = new();4 private int _count = 0;56 public int Count7 {8 get { lock (_lock) { return _count; } }9 }1011 public void Increment()12 {13 lock (_lock)14 {15 _count++;16 }17 }1819 public void Decrement()20 {21 lock (_lock)22 {23 _count--;24 }25 }26}
SemaphoreSlim
A semaphore allows a limited number of concurrent accesses:
csharp1// Allow max 3 concurrent operations2private readonly SemaphoreSlim _semaphore = new(3);34async Task DoWorkAsync()5{6 await _semaphore.WaitAsync(); // Wait for slot7 try8 {9 await PerformOperationAsync();10 }11 finally12 {13 _semaphore.Release(); // Free the slot14 }15}
SemaphoreSlim for Async
Unlike lock, SemaphoreSlim works with async code:
csharp1private readonly SemaphoreSlim _asyncLock = new(1, 1); // Like a lock23async Task SafeOperationAsync()4{5 await _asyncLock.WaitAsync();6 try7 {8 await SomeAsyncWork();9 }10 finally11 {12 _asyncLock.Release();13 }14}
Rate Limiting with SemaphoreSlim
csharp1class RateLimitedClient2{3 private readonly SemaphoreSlim _throttle = new(5); // Max 5 concurrent4 private readonly HttpClient _client = new();56 public async Task<string> GetAsync(string url)7 {8 await _throttle.WaitAsync();9 try10 {11 return await _client.GetStringAsync(url);12 }13 finally14 {15 _throttle.Release();16 }17 }18}1920// Usage - only 5 requests at a time21var client = new RateLimitedClient();22var tasks = urls.Select(url => client.GetAsync(url));23await Task.WhenAll(tasks); // Throttled to 5 concurrent
SemaphoreSlim with Timeout
csharp1var semaphore = new SemaphoreSlim(1);23bool acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(5));4if (!acquired)5{6 throw new TimeoutException("Could not acquire semaphore");7}89try10{11 await DoWork();12}13finally14{15 semaphore.Release();16}
SemaphoreSlim with Cancellation
csharp1await semaphore.WaitAsync(cancellationToken);
lock vs SemaphoreSlim
| Feature | lock | SemaphoreSlim |
|---|---|---|
| Async support | No | Yes |
| Multiple permits | No (1 only) | Yes |
| Timeout | No | Yes |
| Cancellation | No | Yes |
| Performance | Faster | Slightly slower |
Choosing the Right Primitive
csharp1// Use lock for:2// - Simple, quick synchronous operations3// - When you need the fastest option4lock (_lock)5{6 _counter++;7}89// Use SemaphoreSlim(1, 1) for:10// - Async operations11// - When you need timeout/cancellation12await _semaphore.WaitAsync();1314// Use SemaphoreSlim(n) for:15// - Limiting concurrent operations16// - Rate limiting17await _throttle.WaitAsync(); // n slots available
Common Patterns
Read-Modify-Write
csharp1lock (_lock)2{3 var current = _value;4 var newValue = Transform(current);5 _value = newValue;6}
Check-Then-Act
csharp1lock (_lock)2{3 if (!_dictionary.ContainsKey(key))4 {5 _dictionary[key] = CreateValue();6 }7}
Double-Check Locking (Lazy Init)
csharp1private volatile ExpensiveObject? _instance;2private readonly object _lock = new();34public ExpensiveObject Instance5{6 get7 {8 if (_instance == null) // First check (no lock)9 {10 lock (_lock)11 {12 if (_instance == null) // Second check (with lock)13 {14 _instance = new ExpensiveObject();15 }16 }17 }18 return _instance;19 }20}2122// Better: Use Lazy<T>23private readonly Lazy<ExpensiveObject> _lazy = new(() => new ExpensiveObject());24public ExpensiveObject Instance => _lazy.Value;
Deadlock Warning
Be careful not to create deadlocks:
csharp1// DEADLOCK RISK!2lock (_lockA)3{4 lock (_lockB) // Thread 1 holds A, wants B5 {6 // ...7 }8}910// Another thread:11lock (_lockB)12{13 lock (_lockA) // Thread 2 holds B, wants A - DEADLOCK!14 {15 // ...16 }17}
Prevention: Always acquire locks in the same order.
Key Takeaways
lockprovides mutual exclusion (one thread at a time)SemaphoreSlimallows limited concurrent access- Use
lockfor quick sync operations - Use
SemaphoreSlimfor async, timeouts, rate limiting - Always use private, readonly lock objects
- Dispose SemaphoreSlim when done
- Watch out for deadlocks - acquire locks in consistent order