A high-performance, thread-safe keyed locking library for .NET that provides exclusive access synchronization based on string keys. Features automatic cleanup, async support, timeout mechanisms, and dependency injection integration.
$ dotnet add package AnoiKeyedLockA high-performance, low-allocation keyed lock implementation for .NET that ensures exclusive access per string key with automatic resource cleanup.
✅ High Performance: Lock-free reference counting with minimal contention
✅ Low Allocations: Struct-based releaser avoids heap allocations
✅ Automatic Cleanup: Keys and semaphores are automatically removed when no longer needed
✅ Thread-Safe: Fully thread-safe for concurrent access
✅ Flexible API: Support for sync/async, timeouts, and cancellation tokens
✅ String Keys: Optimized for string-based locking with optional case-insensitive comparison
✅ Diagnostics: Built-in IsLocked() and GetActiveKeys() methods for monitoring
✅ IAsyncDisposable: Support for await using syntax on .NET 8+
✅ Multi-targeting: Supports .NET Standard 2.1, .NET 8, .NET 9, and .NET 10
Simply include the KeyedLock.cs file in your project, or build and reference the assembly.
using AnoiKeyedLock;
var keyedLock = new KeyedLock();
// Basic usage
using (var releaser = keyedLock.Lock("myKey"))
{
// Only one thread can execute this block for "myKey" at a time
DoWork();
}
// Async usage
using (var releaser = await keyedLock.LockAsync("myKey"))
{
await DoWorkAsync();
}
// With timeout
if (keyedLock.TryLock("myKey", TimeSpan.FromSeconds(5), out var releaser))
{
using (releaser)
{
DoWork();
}
}
AnoiKeyedLock provides built-in support for dependency injection through the AddKeyedLock() extension method.
First, add the required NuGet package to your project:
dotnet add package Microsoft.Extensions.DependencyInjection
Then register KeyedLock in your DI container:
using AnoiKeyedLock;
using Microsoft.Extensions.DependencyInjection;
// In your Startup.cs or Program.cs
services.AddKeyedLock();
// Or with custom string comparer (e.g., case-insensitive)
services.AddKeyedLock(StringComparer.OrdinalIgnoreCase);
Once registered, inject IKeyedLock into your services:
public class MyService
{
private readonly IKeyedLock _keyedLock;
public MyService(IKeyedLock keyedLock)
{
_keyedLock = keyedLock;
}
public async Task ProcessAsync(string id)
{
using (var releaser = await _keyedLock.LockAsync(id))
{
// Your synchronized code here
await DoWorkAsync(id);
}
}
}
// Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);
// Register KeyedLock as a singleton
builder.Services.AddKeyedLock();
// Register your services
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
// Controller usage
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IKeyedLock _keyedLock;
public UserController(IKeyedLock keyedLock)
{
_keyedLock = keyedLock;
}
[HttpPost("{userId}/process")]
public async Task<IActionResult> ProcessUser(string userId)
{
using (var releaser = await _keyedLock.LockAsync(userId))
{
// Ensure only one request processes this user at a time
await ProcessUserDataAsync(userId);
return Ok();
}
}
}
The KeyedLock is registered as a singleton by default, which is the recommended approach to ensure all parts of your application share the same lock instance for a given key.
// Default (ordinal) string comparison
var keyedLock = new KeyedLock();
// Case-insensitive comparison
var keyedLock = new KeyedLock(StringComparer.OrdinalIgnoreCase);
// Custom concurrency settings (for high-throughput scenarios)
var keyedLock = new KeyedLock(
concurrencyLevel: Environment.ProcessorCount * 2,
initialCapacity: 100,
comparer: StringComparer.Ordinal);
Lock(string key)Acquires a lock for the specified key. Blocks indefinitely until the lock is acquired.
KeyedLockReleaser Lock(string key)
TryLock(string key, TimeSpan timeout, out KeyedLockReleaser releaser)Tries to acquire a lock within the specified timeout.
bool TryLock(string key, TimeSpan timeout, out KeyedLockReleaser releaser)
bool TryLock(string key, int millisecondsTimeout, out KeyedLockReleaser releaser)
TryLock(string key, CancellationToken cancellationToken, out KeyedLockReleaser releaser)Tries to acquire a lock with cancellation support.
bool TryLock(string key, CancellationToken cancellationToken, out KeyedLockReleaser releaser)
LockAsync(string key, CancellationToken cancellationToken = default)Asynchronously acquires a lock for the specified key.
Task<KeyedLockReleaser> LockAsync(string key, CancellationToken cancellationToken = default)
TryLockAsync(string key, TimeSpan timeout, CancellationToken cancellationToken = default)Tries to asynchronously acquire a lock within the specified timeout.
Task<(bool success, KeyedLockReleaser releaser)> TryLockAsync(
string key,
TimeSpan timeout,
CancellationToken cancellationToken = default)
Task<(bool success, KeyedLockReleaser releaser)> TryLockAsync(
string key,
int millisecondsTimeout,
CancellationToken cancellationToken = default)
CountGets the current number of keys being tracked.
int Count { get; }
IsLocked(string key)Checks if a lock is currently held for the specified key. Useful for diagnostics.
bool IsLocked(string key)
⚠️ Note: This is a point-in-time check. The lock state may change immediately after this method returns. Do not use for synchronization decisions.
GetActiveKeys()Gets a snapshot of all keys that currently have active locks. Useful for monitoring and debugging.
string[] GetActiveKeys()
On .NET 8 and later, KeyedLockReleaser implements IAsyncDisposable, enabling the await using syntax:
await using (var releaser = await keyedLock.LockAsync("myKey"))
{
await DoWorkAsync();
}
KeyedLockReleaser is a struct to avoid heap allocations| Operation | Complexity | Allocations |
|---|---|---|
| Lock acquisition | O(1) amortized | ~0 (struct releaser) |
| Lock release | O(1) amortized | 0 |
| Key cleanup | O(1) amortized | 0 |
public class ApiClient
{
private readonly KeyedLock _lock = new KeyedLock();
public async Task<User> GetUserAsync(string userId)
{
using (var releaser = await _lock.LockAsync(userId))
{
return await _httpClient.GetFromJsonAsync<User>($"/users/{userId}");
}
}
}
public class FileProcessor
{
private readonly KeyedLock _lock = new KeyedLock();
public async Task ProcessFileAsync(string filePath)
{
using (var releaser = await _lock.LockAsync(filePath))
{
// Ensure only one thread processes this file at a time
await ProcessFileInternalAsync(filePath);
}
}
}
public class ConnectionManager
{
private readonly KeyedLock _lock = new KeyedLock();
public async Task<T> ExecuteAsync<T>(string connectionString, Func<DbConnection, Task<T>> operation)
{
using (var releaser = await _lock.LockAsync(connectionString))
{
using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
return await operation(connection);
}
}
}
}
public class RateLimiter
{
private readonly KeyedLock _lock = new KeyedLock();
public async Task<bool> TryExecuteAsync(string clientId, Func<Task> action, TimeSpan minInterval)
{
var result = await _lock.TryLockAsync(clientId, TimeSpan.FromMilliseconds(1));
if (result.success)
{
using (result.releaser)
{
await action();
await Task.Delay(minInterval); // Enforce minimum interval
return true;
}
}
return false;
}
}
using statements to ensure locks are releasedSee Examples.cs for comprehensive usage examples including:
MIT
Contributions are welcome! Please feel free to submit pull requests or open issues.