Skip to content

Commit 94e51aa

Browse files
committed
improve
1 parent 26437bd commit 94e51aa

5 files changed

Lines changed: 384 additions & 76 deletions

File tree

Netcorext.Extensions.Threading.Locker.sln

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
44
VisualStudioVersion = 17.0.31903.59
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B8792F7D-87C0-461E-AD7F-6ACFE5261FF9}"
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EAE39435-3728-4173-B6D2-7F0DCBA588F0}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netcorext.Extensions.Threading.Locker", "src\Netcorext.Extensions.Threading.Locker\Netcorext.Extensions.Threading.Locker.csproj", "{D7C407CF-8CE4-4B13-B084-348D3A1504CA}"
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netcorext.Extensions.Threading.Locker", "src\Netcorext.Extensions.Threading.Locker\Netcorext.Extensions.Threading.Locker.csproj", "{005938FC-E15F-4568-ABC4-7556EE7A43B9}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{57156343-36BA-4FF9-82F5-7F18632BE600}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netcorext.Extensions.Threading.Locker.Tests", "tests\Netcorext.Extensions.Threading.Locker.Tests\Netcorext.Extensions.Threading.Locker.Tests.csproj", "{12B2F017-C7B1-4C02-800B-445AF9544E89}"
913
EndProject
1014
Global
1115
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1216
Debug|Any CPU = Debug|Any CPU
1317
Release|Any CPU = Release|Any CPU
1418
EndGlobalSection
15-
GlobalSection(SolutionProperties) = preSolution
16-
HideSolutionNode = FALSE
17-
EndGlobalSection
1819
GlobalSection(ProjectConfigurationPlatforms) = postSolution
19-
{D7C407CF-8CE4-4B13-B084-348D3A1504CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20-
{D7C407CF-8CE4-4B13-B084-348D3A1504CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
21-
{D7C407CF-8CE4-4B13-B084-348D3A1504CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
22-
{D7C407CF-8CE4-4B13-B084-348D3A1504CA}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{005938FC-E15F-4568-ABC4-7556EE7A43B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{005938FC-E15F-4568-ABC4-7556EE7A43B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{005938FC-E15F-4568-ABC4-7556EE7A43B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{005938FC-E15F-4568-ABC4-7556EE7A43B9}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{12B2F017-C7B1-4C02-800B-445AF9544E89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{12B2F017-C7B1-4C02-800B-445AF9544E89}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{12B2F017-C7B1-4C02-800B-445AF9544E89}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{12B2F017-C7B1-4C02-800B-445AF9544E89}.Release|Any CPU.Build.0 = Release|Any CPU
2328
EndGlobalSection
2429
GlobalSection(NestedProjects) = preSolution
25-
{D7C407CF-8CE4-4B13-B084-348D3A1504CA} = {B8792F7D-87C0-461E-AD7F-6ACFE5261FF9}
30+
{005938FC-E15F-4568-ABC4-7556EE7A43B9} = {EAE39435-3728-4173-B6D2-7F0DCBA588F0}
31+
{12B2F017-C7B1-4C02-800B-445AF9544E89} = {57156343-36BA-4FF9-82F5-7F18632BE600}
2632
EndGlobalSection
2733
EndGlobal

src/Netcorext.Extensions.Threading.Locker/KeyLocker.cs

Lines changed: 77 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
1-
using System.Collections.Concurrent;
1+
using System.Collections.Concurrent;
22
using Microsoft.Extensions.Logging;
3+
using Netcorext.Extensions.Threading;
34

4-
namespace Netcorext.Extensions.Threading;
5-
5+
/// <summary>
6+
/// Provides a key-based locking mechanism for controlling concurrent access to resources.
7+
/// </summary>
68
public class KeyLocker : IDisposable
79
{
8-
private bool _disposed;
10+
private volatile int _disposed;
911
private readonly int _maxConcurrent;
1012
private readonly TimeSpan? _timeout;
13+
private readonly TimeSpan _cleanupInterval;
1114
private readonly bool _throwTimeoutException;
12-
private readonly TimeSpan? _expired;
1315
private readonly ILogger _logger;
1416
private readonly ConcurrentDictionary<string, KeyState> _locks = new();
15-
private readonly ConcurrentDictionary<string, Timer> _timers = new();
16-
17-
public KeyLocker(ILogger logger, TimeSpan? timeout = null, bool throwTimeoutException = false, TimeSpan? expired = null, int maxConcurrent = 1)
17+
private readonly Timer _cleanupTimer;
18+
19+
/// <summary>
20+
/// Initializes a new instance of the KeyLocker class.
21+
/// </summary>
22+
/// <param name="logger">The logger instance for recording events.</param>
23+
/// <param name="timeout">Optional timeout for wait operations.</param>
24+
/// <param name="throwTimeoutException">Whether to throw an exception on timeout.</param>
25+
/// <param name="maxConcurrent">Maximum number of concurrent operations allowed.</param>
26+
/// <param name="cleanupInterval">Interval for cleaning up expired locks. Default is 5 minutes.</param>
27+
public KeyLocker(ILogger logger, TimeSpan? timeout = null, bool throwTimeoutException = false, int maxConcurrent = 1, TimeSpan? cleanupInterval = null)
1828
{
19-
_maxConcurrent = maxConcurrent;
29+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
2030
_timeout = timeout;
2131
_throwTimeoutException = throwTimeoutException;
22-
_expired = expired ?? TimeSpan.FromMilliseconds(10 * 60 * 1000);
23-
_logger = logger;
32+
_maxConcurrent = maxConcurrent > 0 ? maxConcurrent : throw new ArgumentException("maxConcurrent must be greater than 0", nameof(maxConcurrent));
33+
_cleanupInterval = cleanupInterval ?? TimeSpan.FromMinutes(5);
34+
_cleanupTimer = new Timer(CleanupIdleLocks, null, _cleanupInterval, _cleanupInterval);
2435
}
2536

2637
public void Wait(string key)
2738
{
28-
var keyState = _locks.AddOrUpdate(key, CreateLockItem, (k, state) =>
39+
var keyState = _locks.AddOrUpdate(key, CreateLockItem, (_, state) =>
2940
{
3041
lock (state)
3142
{
@@ -43,9 +54,6 @@ public void Wait(string key)
4354
if (keyState.ReleaseAll)
4455
return;
4556

46-
if (_expired.HasValue && _timers.TryAdd(key, new Timer(HandleExpired, key, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan)))
47-
_timers[key].Change(_expired.Value, _expired.Value);
48-
4957
if (_timeout.HasValue)
5058
{
5159
if (keyState.Semaphore.Wait(_timeout.Value, keyState.Cancellation.Token))
@@ -63,7 +71,7 @@ public void Wait(string key)
6371

6472
public async Task WaitAsync(string key)
6573
{
66-
var keyState = _locks.AddOrUpdate(key, CreateLockItem, (k, state) =>
74+
var keyState = _locks.AddOrUpdate(key, CreateLockItem, (_, state) =>
6775
{
6876
lock (state)
6977
{
@@ -81,9 +89,6 @@ public async Task WaitAsync(string key)
8189
if (keyState.ReleaseAll)
8290
return;
8391

84-
if (_expired.HasValue && _timers.TryAdd(key, new Timer(HandleExpired, key, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan)))
85-
_timers[key].Change(_expired.Value, _expired.Value);
86-
8792
if (_timeout.HasValue)
8893
{
8994
if (await keyState.Semaphore.WaitAsync(_timeout.Value, keyState.Cancellation.Token))
@@ -135,8 +140,6 @@ public int ReleaseAll(string key)
135140
if (keyState.ReleaseAll || keyState.Cancellation.IsCancellationRequested)
136141
return 0;
137142

138-
keyState.ReleaseAll = true;
139-
140143
var releaseCount = 0;
141144

142145
try
@@ -145,6 +148,7 @@ public int ReleaseAll(string key)
145148
{
146149
try
147150
{
151+
keyState.ReleaseAll = true;
148152
keyState.LastWaitingTime = DateTimeOffset.UtcNow;
149153
keyState.Semaphore.Release();
150154
keyState.DecrementConcurrent();
@@ -167,23 +171,31 @@ public int ReleaseAll(string key)
167171

168172
public void Reset(string key)
169173
{
170-
if (!_locks.TryGetValue(key, out var keyState))
174+
if (!_locks.TryRemove(key, out var keyState))
171175
return;
172176

173177
lock (keyState)
174178
{
175-
keyState.LastWaitingTime = DateTimeOffset.UtcNow;
176-
keyState.Cancellation = new CancellationTokenSource();
177-
keyState.ReleaseAll = false;
178-
}
179+
try
180+
{
181+
while (true)
182+
{
183+
try
184+
{
185+
keyState.Semaphore.Release();
186+
}
187+
catch (SemaphoreFullException)
188+
{
189+
keyState.Cancellation.Cancel(true);
179190

180-
if (!_timers.TryRemove(key, out var timer))
181-
return;
191+
break;
192+
}
193+
}
194+
}
195+
catch (ObjectDisposedException) { }
196+
catch (ArgumentOutOfRangeException) { }
182197

183-
lock (timer)
184-
{
185-
timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
186-
timer.Dispose();
198+
keyState.Dispose();
187199
}
188200
}
189201

@@ -192,43 +204,38 @@ public int GetWaitingCount(string key)
192204
return _locks.TryGetValue(key, out var keyState) ? keyState.WaitingConcurrent : 0;
193205
}
194206

207+
public bool HasLock(string key)
208+
{
209+
return _locks.ContainsKey(key);
210+
}
211+
195212
private void HandleTimeout(KeyState keyState)
196213
{
197-
_logger.LogWarning("Lock on key '{Key}' timed out", keyState.Key);
214+
_logger.LogWarning("Lock on key '{Key}' timed out after {Timeout}", keyState.Key, _timeout);
198215

199216
Release(keyState.Key);
200217

201218
if (_throwTimeoutException)
202-
throw new TimeoutException($"Lock on key '{keyState.Key}' timed out.");
219+
throw new TimeoutException($"Lock operation timed out for key: {keyState.Key}");
203220
}
204221

205-
private void HandleExpired(object? state)
222+
private void CleanupIdleLocks(object? state)
206223
{
207-
if (state is not string key || !_locks.TryGetValue(key, out var keyState))
208-
return;
209-
210-
var elapsed = DateTimeOffset.UtcNow.Subtract(keyState.LastWaitingTime);
224+
var now = DateTimeOffset.UtcNow;
211225

212-
if (!_expired.HasValue || !(elapsed >= _expired))
213-
return;
226+
var idledKeys = _locks
227+
.Where(kvp => now - kvp.Value.LastWaitingTime > _cleanupInterval && !kvp.Value.ReleaseAll)
228+
.Select(kvp => kvp.Key)
229+
.ToArray();
214230

215-
lock (keyState)
231+
foreach (var key in idledKeys)
216232
{
217-
if (!_locks.TryRemove(keyState.Key, out _))
218-
return;
219-
220-
_logger.LogWarning("Key '{Key}' expired({Elapsed}), has been removed", keyState.Key, elapsed);
233+
_logger.LogInformation("Cleaning up idled lock for key '{Key}'", key);
221234

222-
if (_timers.TryRemove(keyState.Key, out var timer))
223-
{
224-
lock (timer)
225-
{
226-
timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
227-
timer.Dispose();
228-
}
229-
}
235+
if (!_locks.TryRemove(key, out var keyState)) continue;
230236

231-
keyState.Dispose();
237+
keyState.Cancellation.Cancel();
238+
keyState.Semaphore.Dispose();
232239
}
233240
}
234241

@@ -248,21 +255,26 @@ private KeyState CreateLockItem(string key)
248255

249256
public void Dispose()
250257
{
251-
if (_disposed)
258+
if (Interlocked.Exchange(ref _disposed, 1) != 0)
252259
return;
253260

254-
_disposed = true;
261+
_cleanupTimer.Change(TimeSpan.Zero, TimeSpan.Zero);
262+
_cleanupTimer.Dispose();
255263

256-
foreach (var key in _locks.Keys)
264+
foreach (var keyState in _locks.Values)
257265
{
258-
if (_locks.TryRemove(key, out var keyState))
259-
keyState.Dispose();
266+
try
267+
{
268+
keyState.Cancellation.Cancel();
269+
keyState.Semaphore.Dispose();
270+
}
271+
catch (Exception ex)
272+
{
273+
_logger.LogError(ex, "Error during KeyLocker disposal");
274+
}
260275
}
261276

262-
foreach (var key in _timers.Keys)
263-
{
264-
if (_timers.TryRemove(key, out var timer))
265-
timer.Dispose();
266-
}
277+
_locks.Clear();
278+
GC.SuppressFinalize(this);
267279
}
268280
}

src/Netcorext.Extensions.Threading.Locker/Netcorext.Extensions.Threading.Locker.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<LangVersion>latest</LangVersion>
66
<VersionMajor>1</VersionMajor>
77
<VersionMinor>0</VersionMinor>
8-
<VersionPatch>2</VersionPatch>
8+
<VersionPatch>3</VersionPatch>
99
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
1010
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
1111
<FileVersion>$(VersionMajor).$([System.DateTime]::UtcNow.ToString(yy)).$([System.DateTime]::UtcNow.ToString(MMdd)).$([System.DateTime]::UtcNow.ToString(HHmm))</FileVersion>

0 commit comments

Comments
 (0)