AnoiKeyedLock 1.0.0
See the version list below for details.
dotnet add package AnoiKeyedLock --version 1.0.0
NuGet\Install-Package AnoiKeyedLock -Version 1.0.0
<PackageReference Include="AnoiKeyedLock" Version="1.0.0" />
<PackageVersion Include="AnoiKeyedLock" Version="1.0.0" />
<PackageReference Include="AnoiKeyedLock" />
paket add AnoiKeyedLock --version 1.0.0
#r "nuget: AnoiKeyedLock, 1.0.0"
#:package AnoiKeyedLock@1.0.0
#addin nuget:?package=AnoiKeyedLock&version=1.0.0
#tool nuget:?package=AnoiKeyedLock&version=1.0.0
AnoiKeyedLock
A high-performance, low-allocation keyed lock implementation for .NET that ensures exclusive access per string key with automatic resource cleanup.
Features
✅ 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
✅ .NET Standard 2.1: Compatible with .NET Core 3.0+, .NET 5+, and modern platforms
Installation
Simply include the KeyedLock.cs file in your project, or build and reference the assembly.
Quick Start
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();
}
}
API Reference
Constructor
// Default (ordinal) string comparison
var keyedLock = new KeyedLock();
// Case-insensitive comparison
var keyedLock = new KeyedLock(StringComparer.OrdinalIgnoreCase);
Synchronous Methods
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)
Asynchronous Methods
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)
Properties
Count
Gets the current number of keys being tracked.
int Count { get; }
How It Works
Architecture
- Concurrent Dictionary: Stores semaphores keyed by string keys
- Reference Counting: Tracks how many threads are waiting for or holding each lock
- Automatic Cleanup: When the reference count reaches zero, the semaphore is disposed and removed
- Struct-based Releaser: The
KeyedLockReleaseris a struct to avoid heap allocations
Thread Safety
- Multiple threads can acquire locks on different keys simultaneously without blocking
- Multiple threads waiting on the same key will queue in FIFO order
- Reference counting uses lock-free atomic operations (Interlocked)
- Dictionary operations use ConcurrentDictionary for thread-safe access
Performance Characteristics
| Operation | Complexity | Allocations |
|---|---|---|
| Lock acquisition | O(1) amortized | ~0 (struct releaser) |
| Lock release | O(1) amortized | 0 |
| Key cleanup | O(1) amortized | 0 |
Use Cases
Prevent Duplicate API Calls
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}");
}
}
}
File Processing Coordination
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);
}
}
}
Database Connection Management
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);
}
}
}
}
Rate Limiting
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;
}
}
Best Practices
- ✅ Always use
usingstatements to ensure locks are released - ✅ Keep critical sections short to minimize contention
- ✅ Use async methods for I/O operations to avoid blocking threads
- ✅ Keys cannot be null or whitespace - validation is performed automatically
- ✅ Handle timeouts gracefully - always have a fallback strategy
- ❌ Avoid nested locks on the same key (not reentrant)
- ❌ Don't hold locks across long-running operations without timeouts
Requirements
- .NET Standard 2.1 or higher
- C# 7.3 or higher
Compatible With
- .NET Core 3.0+
- .NET 5, 6, 7, 8, 9+
- Xamarin (iOS 12.16+, Android 10.0+)
- Unity 2021.2+
Examples
See Examples.cs for comprehensive usage examples including:
- Basic synchronous and asynchronous locking
- Timeout handling
- Cancellation support
- Real-world scenarios (file processing, API calls, etc.)
- Performance testing
License
[Your License Here]
Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial release with full keyed locking functionality, async/await support, and dependency injection integration.