Keyed locks
editA keyed lock is a set of locks, one per key: two callers that use the same key wait for each other, and callers with different keys run concurrently. Use it to serialize the operations on one user, one file or one cache entry without serializing the whole application.
In asynchronous code
KeyedAsyncLock<TKey>.LockAsync waits asynchronously for the lock of a key and returns a lease that releases it:
var keyedLock = new KeyedAsyncLock<string>(StringComparer.Ordinal);
var updateCounts = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
async Task ProcessUserAsync(string userId)
{
using (await keyedLock.LockAsync(userId))
{
// Only one operation per userId at a time
// Multiple different userIds can be processed concurrently
var count = updateCounts.GetValueOrDefault(userId);
await Task.Yield();
updateCounts[userId] = count + 1;
}
}
await Task.WhenAll(ProcessUserAsync("alice"), ProcessUserAsync("alice"), ProcessUserAsync("bob"));
Assert.Equal(2, updateCounts["alice"]);
Assert.Equal(1, updateCounts["bob"]);- The constructor takes the
IEqualityComparer<TKey>that decides whether two keys are the same lock, for instanceStringComparer.OrdinalIgnoreCasefor case-insensitive keys. Passnullto use the default comparer of the key type. LockAsync(key, cancellationToken)accepts aCancellationTokento give up waiting.- Only the first disposal of a lease releases the key.
In synchronous code
KeyedLock<TKey>.Lock blocks the calling thread until the lock of the key is free, and returns an IDisposable that releases it:
var keyedLock = new KeyedLock<string>(StringComparer.Ordinal);
var updateCounts = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
void ProcessUser(string userId)
{
using (keyedLock.Lock(userId))
{
// Only one operation per userId at a time
// Multiple different userIds can be processed concurrently
var count = updateCounts.GetValueOrDefault(userId);
updateCounts[userId] = count + 1;
}
}
Parallel.Invoke(() => ProcessUser("alice"), () => ProcessUser("alice"), () => ProcessUser("bob"));
Assert.Equal(2, updateCounts["alice"]);
Assert.Equal(1, updateCounts["bob"]);KeyedLock<TKey> is thread-affine: the returned object must be disposed on the thread that called Lock. Disposing it on another thread throws a SynchronizationLockException and leaves the lock held. The critical section must therefore not contain an await; use KeyedAsyncLock<TKey> in asynchronous code.
Memory use
The locks only track the keys that are held or waited for: the state of a key is removed when its last lease is released. A keyed lock used with many distinct keys over time does not grow with the number of keys it has seen.