Skip to content

Commit

Permalink
Fixed serious issue with concurrent threads having been limited to 1 …
Browse files Browse the repository at this point in the history
…irrespective of setting in the constructor. Also added a few features and tests.
  • Loading branch information
MarkCiliaVincenti committed Oct 16, 2022
1 parent 8d0e7de commit bb1d3a2
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 8 deletions.
28 changes: 28 additions & 0 deletions AsyncKeyedLock.Tests/AsyncKeyedLock.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AsyncKeyedLock\AsyncKeyedLock.csproj" />
</ItemGroup>

</Project>
76 changes: 76 additions & 0 deletions AsyncKeyedLock.Tests/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Concurrent;
using Xunit;

namespace AsyncKeyedLock.Tests
{
public class Tests
{
[Fact]
public async Task Test1AtATime()
{
var range = 25;
var asyncKeyedLocker = new AsyncKeyedLocker();
var concurrentQueue = new ConcurrentQueue<int>();

var tasks = Enumerable.Range(1, range * 2)
.Select(async i =>
{
var key = Convert.ToInt32(Math.Ceiling((double)i / 2));
using (await asyncKeyedLocker.LockAsync(key))
{
concurrentQueue.Enqueue(key);
await Task.Delay(100 * key);
}
});
await Task.WhenAll(tasks.AsParallel());

bool valid = true;
var list = concurrentQueue.ToList();

for (int i = 0; i < range; i++)
{
if (list[i] != list[i + range])
{
valid = false;
break;
}
}

Assert.True(valid);
}

[Fact]
public async Task Test2AtATime()
{
var range = 4;
var asyncKeyedLocker = new AsyncKeyedLocker(2);
var concurrentQueue = new ConcurrentQueue<int>();

var tasks = Enumerable.Range(1, range * 4)
.Select(async i =>
{
var key = Convert.ToInt32(Math.Ceiling((double)i / 4));
using (await asyncKeyedLocker.LockAsync(key))
{
concurrentQueue.Enqueue(key);
await Task.Delay((100 * key) + 1000);
}
});
await Task.WhenAll(tasks.AsParallel());

bool valid = true;
var list = concurrentQueue.ToList();

for (int i = 0; i < range * 2; i++)
{
if (list[i] != list[i + (range * 2)])
{
valid = false;
break;
}
}

Assert.True(valid);
}
}
}
8 changes: 7 additions & 1 deletion AsyncKeyedLock.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.31903.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncKeyedLock", "AsyncKeyedLock\AsyncKeyedLock.csproj", "{0A7CB651-70B8-4C5C-8647-31B211273EAE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncKeyedLock", "AsyncKeyedLock\AsyncKeyedLock.csproj", "{0A7CB651-70B8-4C5C-8647-31B211273EAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncKeyedLock.Tests", "AsyncKeyedLock.Tests\AsyncKeyedLock.Tests.csproj", "{0402C044-81BB-4029-BE63-168D3DFABA99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -15,6 +17,10 @@ Global
{0A7CB651-70B8-4C5C-8647-31B211273EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A7CB651-70B8-4C5C-8647-31B211273EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A7CB651-70B8-4C5C-8647-31B211273EAE}.Release|Any CPU.Build.0 = Release|Any CPU
{0402C044-81BB-4029-BE63-168D3DFABA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0402C044-81BB-4029-BE63-168D3DFABA99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0402C044-81BB-4029-BE63-168D3DFABA99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0402C044-81BB-4029-BE63-168D3DFABA99}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
8 changes: 4 additions & 4 deletions AsyncKeyedLock/AsyncKeyedLock.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
<RepositoryUrl>https://github.com/MarkCiliaVincenti/AsyncKeyedLock.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/MarkCiliaVincenti/AsyncKeyedLock</PackageProjectUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>2.0.3</Version>
<Version>3.0.0</Version>
<PackageIcon>logo.png</PackageIcon>
<PackageReleaseNotes>Enabled trimming.</PackageReleaseNotes>
<PackageReleaseNotes>Fixed serious issue with concurrent threads having been limited to 1 irrespective of setting in the constructor. Also added a few features and tests.</PackageReleaseNotes>
<Description>An asynchronous .NET Standard 2.0 library that allows you to lock based on a key (keyed semaphores), only allowing a defined number of concurrent threads that share the same key.</Description>
<Copyright>© 2022 Mark Cilia Vincenti</Copyright>
<PackageTags>async,lock,key,semaphore,dictionary</PackageTags>
<RepositoryType>git</RepositoryType>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<AssemblyVersion>2.0.3.0</AssemblyVersion>
<FileVersion>2.0.3.0</FileVersion>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<IsPackable>true</IsPackable>
<IsTrimmable>true</IsTrimmable>
Expand Down
39 changes: 37 additions & 2 deletions AsyncKeyedLock/AsyncKeyedLocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private SemaphoreSlim GetOrAdd(object key)
}
else
{
referenceCounter = new ReferenceCounter<SemaphoreSlim>(new SemaphoreSlim(1, MaxCount));
referenceCounter = new ReferenceCounter<SemaphoreSlim>(new SemaphoreSlim(MaxCount));
SemaphoreSlims[key] = referenceCounter;
}
}
Expand All @@ -63,16 +63,41 @@ public IDisposable Lock(object key)
/// <returns>A disposable value.</returns>
public async Task<IDisposable> LockAsync(object key)
{
await GetOrAdd(key).WaitAsync().ConfigureAwait(false);
var semaphoreSlim = GetOrAdd(key);
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
return new Releaser(this, key);
}

/// <summary>
/// Checks whether or not there is a thread making use of a keyed lock.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns><see langword="true"/> if the key is in use; otherwise, false.</returns>
public bool IsInUse(object key)
{
lock (SemaphoreSlims)
{
return SemaphoreSlims.ContainsKey(key);
}
}

/// <summary>
/// Get the number of requests concurrently locked for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of requests.</returns>
[Obsolete("This method should not longer be used as it is confusing with Semaphore terminology. Use <see cref=\"GetCurrentCount\"/> or <see cref=\"GetRemaningCount\"/> instead depending what you want to do.")]
public int GetCount(object key)
{
return GetRemainingCount(key);
}

/// <summary>
/// Get the number of requests concurrently locked for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of requests concurrently locked for a given key.</returns>
public int GetRemainingCount(object key)
{
lock (SemaphoreSlims)
{
Expand All @@ -84,6 +109,16 @@ public int GetCount(object key)
}
}

/// <summary>
/// Get the number of remaining threads that can enter the lock for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of remaining threads that can enter the lock for a given key.</returns>
public int GetCurrentCount(object key)
{
return MaxCount - GetRemainingCount(key);
}

/// <summary>
/// Forces requests to be released from the semaphore.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions AsyncKeyedLock/IAsyncKeyedLocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,35 @@ public interface IAsyncKeyedLocker
/// <returns>A disposable value.</returns>
Task<IDisposable> LockAsync(object key);

/// <summary>
/// Checks whether or not there is a thread making use of a keyed lock.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns><see langword="true"/> if the key is in use; otherwise, false.</returns>
bool IsInUse(object key);

/// <summary>
/// Get the number of requests concurrently locked for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of requests.</returns>
[Obsolete("This method should not longer be used as it is confusing with Semaphore terminology. Use <see cref=\"GetCurrentCount\"/> or <see cref=\"GetRemaningCount\"/> instead depending what you want to do.")]
int GetCount(object key);

/// <summary>
/// Get the number of requests concurrently locked for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of requests concurrently locked for a given key.</returns>
int GetRemainingCount(object key);

/// <summary>
/// Get the number of remaining threads that can enter the lock for a given key.
/// </summary>
/// <param name="key">The key requests are locked on.</param>
/// <returns>The number of remaining threads that can enter the lock for a given key.</returns>
int GetCurrentCount(object key);

/// <summary>
/// Forces requests to be released from the semaphore.
/// </summary>
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ var asyncKeyedLocker = new AsyncKeyedLocker(2);

If you would like to see how many concurrent requests there are for a semaphore for a given key:
```csharp
int myCount = asyncKeyedLocker.GetCount(myObject);
int myRemainingCount = asyncKeyedLocker.GetRemainingCount(myObject);
```

If you would like to see the number of remaining threads that can enter the lock for a given key:
```csharp
int myCurrentCount = asyncKeyedLocker.GetCurrentCount(myObject);
```

If you would like to check whether any request is using a specific key:
```csharp
bool isInUse = asyncKeyedLocker.IsInUse(myObject);
```

And if for some reason you need to force release the requests in the semaphore for a key:
Expand Down

0 comments on commit bb1d3a2

Please sign in to comment.