Giter VIP home page Giter VIP logo

markciliavincenti / asynckeyedlock Goto Github PK

View Code? Open in Web Editor NEW
138.0 6.0 8.0 265 KB

An asynchronous .NET Standard 2.0 library that allows you to lock based on a key (keyed semaphores), limiting concurrent threads sharing the same key to a specified number, with optional pooling for reducing memory allocations.

Home Page: https://www.nuget.org/packages/AsyncKeyedLock

License: MIT License

C# 100.00%
async lock key semaphore semaphoreslim keyed semaphores duplicate pooling synchronization

asynckeyedlock's Introduction

AsyncKeyedLock AsyncKeyedLock

GitHub Workflow Status NuGet NuGet Codacy Grade Codecov

An asynchronous .NET Standard 2.0 library that allows you to lock based on a key (keyed semaphores), limiting concurrent threads sharing the same key to a specified number, with optional pooling for reducing memory allocations.

For example, suppose you were processing financial transactions, but while working on one account you wouldn't want to concurrently process a transaction for the same account. Of course, you could just add a normal lock, but then you can only process one transaction at a time. If you're processing a transaction for account A, you may want to also be processing a separate transaction for account B. That's where AsyncKeyedLock comes in: it allows you to lock but only if the key matches.

The library uses two very different methods for locking, AsyncKeyedLocker which uses an underlying ConcurrentDictionary that's cleaned up after use and StripedAsyncKeyedLocker which uses a technique called striped locking. Both have their advantages and disadvantages, and in order to help you choose you are highly recommended to read about it in the wiki.

A simple non-keyed lock is also available through AsyncNonKeyedLocker.

Installation and usage

Using this library is straightforward. Here's a simple example for using AsyncKeyedLocker:

private static readonly AsyncKeyedLocker<string> _asyncKeyedLocker = new(o =>
  {
    o.PoolSize = 20; // this is NOT a concurrency limit
    o.PoolInitialFill = 1;
  });

...

using (await _asyncKeyedLocker.LockAsync("test123"))
{
  ...
}

This libary also supports conditional locking, whether for AsyncKeyedLocker, StripedAsyncKeyedLocker or AsyncNonKeyedLocker. This could provide a workaround for reentrancy in some scenarios for example in recursion:

double factorial = Factorial(number);

public static double Factorial(int number, bool isFirst = true)
{
  using (await _asyncKeyedLocker.ConditionalLockAsync("test123", isFirst))
  {
    if (number == 0)
      return 1;
    return number * Factorial(number-1, false);
  }
}

For more help with AsyncKeyedLocker or for examples with StripedAsyncKeyedLocker or AsyncNonKeyedLocker (for simple, non-keyed locking), please take a look at our wiki.

Benchmarks

This library has been extensively benchmarked against several other options and our benchmarks run publicly and transparently on Github Actions.

Credits

Check out our list of contributors!

asynckeyedlock's People

Contributors

c0nd3v avatar dependabot[bot] avatar magicandre1981 avatar markciliavincenti avatar markciliavincenti-cleverbit avatar syzuna avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

asynckeyedlock's Issues

Can i use this library for these use cases?

hi,

I have some race conditions in my middleware that writes sitemaps for my project and another in IRouteConstraint where i sometimes get exception on double ef calls what's the best way to counter this situation.
I want the middleware to lock file writing request until its finished and make other users to wait for it.

is RabbitMQ queuing ideal for such situations although I think RabbitMQ is overkill hence I am looking for alternative like yours.
what's the best approach here I am stuck between choosing something that won't have a negative impact on project.
there is lot of suggestions on google like semaphore async read write lock etc.

NullReferenceException when accessing from Dependency Injection using Azure Functions

When accessing the locker via dependency injection in Azure Functions using
services.AddSingleton<AsyncKeyedLocker>(); or
services.AddSingleton<AsyncKeyedLocker<string>>(); the following exception occurs.

System.NullReferenceException: Object reference not set to an instance of an object.
   at lambda_method338(Closure , IResolverContext )
   at DryIoc.Factory.<>c__DisplayClass26_0.<ApplyReuse>b__2() in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6605
   at DryIoc.Scope.TryGetOrAdd(ImMap`1 items, Int32 id, CreateScopedValue createValue, Int32 disposalOrder) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7849
   at DryIoc.Scope.GetOrAdd(Int32 id, CreateScopedValue createValue, Int32 disposalOrder) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7834
   at DryIoc.Factory.ApplyReuse(Expression serviceExpr, Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6604
   at DryIoc.Factory.GetExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6564
   at DryIoc.Factory.GetDelegateOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6634
   at DryIoc.Container.ResolveAndCacheDefaultFactoryDelegate(Type serviceType, IfUnresolved ifUnresolved) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 210
   at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, IfUnresolved ifUnresolved) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 195
   at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.JobHostServiceProvider.GetService(Type serviceType, IfUnresolved ifUnresolved) in /_/src/WebJobs.Script.WebHost/DependencyInjection/JobHostServiceProvider.cs:line 99
   at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.JobHostServiceProvider.GetService(Type serviceType) in /_/src/WebJobs.Script.WebHost/DependencyInjection/JobHostServiceProvider.cs:line 77
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)

 at USERCODE.CS
   at lambda_method337(Closure , IResolverContext )
   at DryIoc.Factory.<>c__DisplayClass26_0.<ApplyReuse>b__2() in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6605
   at DryIoc.Scope.TryGetOrAdd(ImMap`1 items, Int32 id, CreateScopedValue createValue, Int32 disposalOrder) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7849
   at DryIoc.Scope.GetOrAdd(Int32 id, CreateScopedValue createValue, Int32 disposalOrder) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7834
   at DryIoc.Factory.ApplyReuse(Expression serviceExpr, Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6604
   at DryIoc.Factory.GetExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6564
   at DryIoc.ReflectionFactory.CreateExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7083
   at DryIoc.Factory.GetExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6554
   at DryIoc.ReflectionFactory.CreateExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 7083
   at DryIoc.Factory.GetExpressionOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6554
   at DryIoc.Factory.GetDelegateOrDefault(Request request) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 6634
   at DryIoc.Container.ResolveAndCacheDefaultFactoryDelegate(Type serviceType, IfUnresolved ifUnresolved) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 210
   at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, IfUnresolved ifUnresolved) in /_/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs:line 195
   at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.ScopedServiceProvider.GetService(Type serviceType) in /_/src/WebJobs.Script.WebHost/DependencyInjection/ScopedServiceProvider.cs:line 25
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
   at lambda_method291(Closure , IServiceProvider , Object[] )
   at Microsoft.Azure.WebJobs.Host.Executors.DefaultJobActivator.CreateInstance[T](IServiceProvider serviceProvider) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\DefaultJobActivator.cs:line 42
   at Microsoft.Azure.WebJobs.Host.Executors.DefaultJobActivator.CreateInstance[T](IFunctionInstanceEx functionInstance) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\DefaultJobActivator.cs:line 32
   at Microsoft.Azure.WebJobs.Host.Executors.ActivatorInstanceFactory`1.<>c__DisplayClass1_1.<.ctor>b__0(IFunctionInstanceEx i) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\ActivatorInstanceFactory.cs:line 20
   at Microsoft.Azure.WebJobs.Host.Executors.ActivatorInstanceFactory`1.Create(IFunctionInstanceEx functionInstance) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\ActivatorInstanceFactory.cs:line 26
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`2.CreateInstance(IFunctionInstanceEx functionInstance) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionInvoker.cs:line 44
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ParameterHelper.Initialize() in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 793
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync(IFunctionInstance functionInstance, CancellationToken cancellationToken) in D:\a\_work\1\s\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 104

Ive created a repository that reproduces the issue here - https://github.com/gorillapower/AsyncKeyedLockerDITest. It seems to only be occuring for Azure Functions, when using ASP.NET core, it works fine.

I am also able to use new AsyncKeyedLocker<string>() explicitly as a workaround.

Enable deterministic build

Add DotNet.ReproducibleBuilds to make deterministic builds:

Deterministic builds ensure that the same binary is produced regardless of the machine building it, including paths to sources stored in the symbols.

With this package all settings are enabled, PDBs and sourcelink infos are embedded into the DLL. With this you can step into source code without cloning the repo and using a self compiled DLL.

ArgumentOufOfRangeException although passed value is valid

Affected Version: >= 6.0.1 (5.x works)

Stacktrace:

Unhandled exception. System.ArgumentOutOfRangeException: MaxCount should be greater than or equal to 1. (Parameter 'options')
Actual value was 1.
   at AsyncKeyedLock.AsyncKeyedLockDictionary`1..ctor(AsyncKeyedLockOptions options)
   at AsyncKeyedLock.AsyncKeyedLocker`1..ctor(AsyncKeyedLockOptions options)

It is obvious from the stacktrace that the check seems to be behaving incorrectly.
The value that was passed is 1 and is greater than or equal to 1 obviously.

Code that leads to the error:

new AsyncKeyedLocker<string>(new AsyncKeyedLockOptions(1, 25))

Race condition in the AsyncKeyedLockDictionary<TKey> class

I think that there is a race condition in the AsyncKeyedLockDictionary<TKey> class. Here is the scenario:

  1. The thread-A calls the GetOrAdd(key) method.
  2. The thread-B calls the GetOrAdd(key) method with the same key, invokes the command TryGetValue(key, out var releaser), and then gets suspended by the OS. The releaser is the same instance that is already used by the thread-A.
  3. The thread-A calls the Release(releaser) method, which removes the releaser from the concurrent dictionary, and puts it in the _pool.
  4. The thread-B is resumed by the OS, invokes the command releaser.TryIncrement(), and later calls also the Release(releaser) method for the same releaser instance.

Possible effects of the race condition:

  1. The locking functionality is broken for this key, because a third thread that will call the GetOrAdd(key) with the same key, will not find a releaser in the dictionary, and so it will get one from the _pool, which might not be the same releaser that the thread-B is using, and so it will Wait on a different semaphore.
  2. The locking functionality will malfunction for a different key. Another thread will get the active releaser from the _pool, and so it will Wait without reason until an unrelated key is released.
  3. The same releaser will end up being stored twice in the _pool, unless the _pool has the built-in functionality of detecting duplicates (I didn't look if it does). So the initial race occurrence might have long-lasting consequences. The mechanism won't be self-healed by the passage of time.

Question: What am I missing here?

I'm trying to leverage AsyncKeyedLock to "debounce" some API calls coming from the outside. I want to lock on the Guid Id of the incoming object. The library seems straight-forward, but I wrote a test to try to simulate our situation just to be sure, and I can't get it to act the way I think it should. Here's an isolated test to illustrate what I'm seeing.

    [Test]
    public void ParallelVersion()
    {
        var output = new ConcurrentQueue<(int Ordinal, long Start, long Stop)>();
        var stopwatch = Stopwatch.StartNew();

        Parallel.Invoke(
            () => ProcessEntry(Guid.Parse("00000000-0000-0000-0000-000000000000"), 0).Wait(),
            () => ProcessEntry(Guid.Parse("00000000-0000-0000-0000-000000000000"), 2).Wait(),
            () => ProcessEntry(Guid.Parse("00000000-0000-0000-0000-000000000000"), 4).Wait(),
            () => ProcessEntry(Guid.Parse("11111111-1111-1111-1111-111111111111"), 1).Wait(),
            () => ProcessEntry(Guid.Parse("11111111-1111-1111-1111-111111111111"), 3).Wait(),
            () => ProcessEntry(Guid.Parse("11111111-1111-1111-1111-111111111111"), 5).Wait()
        );

        return;

        async Task ProcessEntry(Guid id, int ordinal)
        {
            using (await SUT.LockAsync(id.ToString()))
            {
                var start = stopwatch.ElapsedMilliseconds;
                Console.WriteLine($@"Processing ordinal {ordinal} at {stopwatch.ElapsedMilliseconds}");
                Thread.Sleep(1000);
                var stop = stopwatch.ElapsedMilliseconds;
                output.Enqueue((ordinal, start, stop));
                Console.WriteLine($@"Processed ordinal {ordinal} at {stopwatch.ElapsedMilliseconds}");
            }
        }
    }

What I expect to see is that none of the even numbered entries should be processed at the same time, and the same for the odds. I expect one of the evens to start processing, and the other two to wait in line. Meanwhile, one of the odds should also start processing while the other two wait in line. When the first even/odd pair have finished, I would expect another two to get picked up, and so on.

I don't really expect to see them line up 0-5 in the output because Parallel.Invoke isn't going to guarantee order. What I'm seeing in the output queue, though, is that every single entry started processing at the exact same time. It could be that I'm outrunning the library's lock itself, in which case I'm out of luck. The other day we saw two identical calls to our API 2ms apart, so that's the kind of timing I'm trying to guard against. Any guidance would be appreciated.

New builds?

Any chance we'll get a new build with all the latest libraries?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.