【问题标题】:Resource concurrency, allow access to one or multiple threads per given resource资源并发,允许每个给定资源访问一个或多个线程
【发布时间】:2021-05-31 12:07:09
【问题描述】:

以下是我的问题。

我有一个 API 控制器,里面有一个 API 端点(资源)。

 api/myResource/{id}/do-something

我正在开发一个中间件,它将根据一些业务规则限制对该资源的访问。在这个中间件中,我正在匹配传入的请求,我正在解析 URI,并且我想允许访问它(让管道流动)或简单地返回 412 状态代码以防达到允许的线程数限制 FOR给定的资源(例如)

 api/myResource/1/do-something /// should allow 2 concurrent accesses.
 api/myResource/2/do-something /// should allow 10 concurrent accesses.
 api/myResource/3/do-something /// should allow 1 concurrent accesses.

为此,我已经开始实施我将附上的解决方案。

internal class AsyncLock<TKey>
{
    private static readonly ConcurrentDictionary<TKey, SemaphoreSlim> _safeSemaphores
        = new ConcurrentDictionary<TKey, SemaphoreSlim>();

    internal async Task<SemaphoreSlim> TryLockAsync(TKey key, int maxConcurrentCount)
    {
        if (!_safeSemaphores.TryGetValue(key, out SemaphoreSlim semaphore))
        {
            semaphore = new SemaphoreSlim(maxConcurrentCount, maxConcurrentCount);

            _safeSemaphores.TryAdd(key, semaphore);
        }

        await semaphore.WaitAsync();

        return semaphore;
    }

    internal SemaphoreSlim TryLock(TKey key, int maxConcurrentCount)
    {
        if (!_safeSemaphores.TryGetValue(key, out SemaphoreSlim semaphore))
        {
            semaphore = new SemaphoreSlim(maxConcurrentCount, maxConcurrentCount);

            _safeSemaphores.TryAdd(key, semaphore);
        }

        semaphore.Wait();

        return semaphore;
    }
}

这就是它的使用方式(它允许2个并发访问,当然这是这个问题的主题,它不会是硬编码的2而是在管道的早期确定)

AsyncLock<string> _lock = new AsyncLock<string>();
SemaphoreSlim semaphore = await _lock .TryLockAsync(key, 2);

if (semaphore.CurrentCount != 0)
{
    context.Response.StatusCode = 200;
    //await _next(context);
    semaphore.Release();
}
else
{
    context.Response.StatusCode = 412;
}

它的行为方式是不可预测的。我正在使用 4 个线程进行测试,有时它按预期工作,有时它们都返回 200,有时它们都卡住了,我的意思是每次都是组合。

如果能帮我解决这个问题,我将不胜感激。

【问题讨论】:

  • 嗨,有趣,也许使用Interlocked.CompareExchange - 如果值已经更新,请跳过。
  • 您知道在锁中使用异步代码会导致应用程序无响应吗?异步代码主要用于 IO 操作。 (文件/网络/等)如果异步代码试图下载一个大文件怎么办?锁会导致拥塞,因此请确保它们尽可能短。我建议审查设计并将异步代码放在锁之外。
  • 为了响应单个 API 调用,是否有可能需要获取多个信号量?还是保证每个api/myResource/X/do-something 将只获得一个信号量?在第二种情况下,您可能需要KeyedSemaphore。您可以查看这个问题作为起点:Asynchronous locking based on a key
  • @TheodorZoulias,我知道这个实现,实际上这是我开始的地方。您的假设是,api/myResource/X/do-something 将被单个信号量获取是正确的。
  • 那里发布了一些可靠的实现。例如,您可以取Stephen Cleary's solution,并将GetOrCreate(object key) 更改为GetOrCreate(object key, int maximumConcurrency),并使用item = new RefCounted&lt;SemaphoreSlim&gt;(new SemaphoreSlim(1, 1)); 行中的参数,替换1s。

标签: c# multithreading asynchronous concurrency locking


【解决方案1】:

使用Monitor.TryEnter 似乎最简单。

object _lock = new object();


void RunIfNotLocked()
{
    bool lockAcquired = false;

    Monitor.TryEnter(_lock, ref lockAcquired);
    if ( !lockAcquired ) 
    {
        //Skip
        return;
    }
    try
    {
        DoSomething();
    }
    finally
    {         
        Monitor.Exit(_lock);
    }
}

【讨论】:

  • Spinlock.TryEnter... 如果在调用 TryEnter 时锁不可用,它将立即返回,无需进一步旋转。
  • 这个实现将限制对单个资源的访问,而我需要能够允许 N 个线程访问相同的资源。
  • “限制对单个资源的访问”和“允许 N 个线程访问同一资源”并不是对立的。你是说资源不止一种?还是说必须允许多个线程同时访问(即不跳过)单个资源?
  • 这让我觉得与你原来的问题有很大不同。我会邀请您编辑和更新您的帖子,但它似乎已作为副本关闭。
  • 哇,现在你的问题完全不同了。现在有一个客户端和一个服务器。对于这个问题,您根本不应该使用lockmonitorsemaphore 或任何类似的东西,因为它们在内存中并且不会扩展,除非您使用某种会话粘性。您的问题更多是关于自定义速率限制方案而不是线程锁。
【解决方案2】:

我已经提出了这个解决方案,它似乎按预期工作。

internal sealed class AsyncLock<TKey>
{
    public readonly Dictionary<TKey, SemaphoreSlim> _semaphores = new Dictionary<TKey, SemaphoreSlim>();

    internal IDisposable Lock(TKey key, int maxDegreeOfParallelism)
    {
        bool acquired = GetOrAdd(key, maxDegreeOfParallelism).Wait(0);

        return acquired
            ? new Releaser(key, this)
            : null;
    }

    internal async Task<IDisposable> LockAsync(TKey key, int maxDegreeOfParallelism = 1)
    {
        bool acquired = await GetOrAdd(key, maxDegreeOfParallelism).WaitAsync(0);

        return acquired
            ? new Releaser(key, this)
            : null;
    }

    private SemaphoreSlim GetOrAdd(TKey key, int maxConcurrencyCount = 1)
    {
        lock (_semaphores)
        {
            if (!_semaphores.TryGetValue(key, out SemaphoreSlim semaphore))
            {
                _semaphores[key] = semaphore = new SemaphoreSlim(maxConcurrencyCount, maxConcurrencyCount);
            }

            return semaphore;
        }
    }

    private sealed class Releaser : IDisposable
    {
        private AsyncLock<TKey> _parent;

        public void Dispose()
        {
            lock (_parent._semaphores)
            {
                if (_parent._semaphores.TryGetValue(Key, out SemaphoreSlim semaphore)) semaphore.Release();
            }
        }

        public TKey Key { get; }

        public Releaser(TKey key, AsyncLock<TKey> parent)
        {
            Key = key;
            _parent = parent;
        }
    }
}

使用它:

AsyncLock<string> _asyncLock = new AsyncLock<string>();

IDisposable disposable = _asyncLock.Lock(key, maxConcurrency: 2);

if (disposable is null)
{
    /// thread skipped
}
else
{
    ///  thread entered
    disposable.Dispose();
}

【讨论】:

  • 不错!您可能应该注意Releaser.DisposeTryGetValue 方法的返回值。根据您当前的实现,这永远不应该是false,但为了安全起见,我还是会进行检查。此外,_semaphoresstatic 字段可能是一个设计缺陷。它会阻止您创建 AsyncLock 类的两个独立实例。
  • 感谢@TheodorZoulias 的输入,我会考虑调整它。
  • 现在完美了!一个小问题:术语“并行度”通常适用于在多个 CPU 内核上并行运行的代码。对于并发异步操作,通常是not running anywhere,更合适的术语是“并发级别”或“最大并发”。
【解决方案3】:

这是您可以使用的KeyedNamedSemaphore 类。它为每个密钥存储一个 named Semaphore。创建密钥后,关联的信号量将保留在字典中,直到释放 KeyedNamedSemaphore 实例。

public class KeyedNamedSemaphore<TKey> : IDisposable
{
    private readonly ConcurrentDictionary<TKey, Semaphore> _perKey;
    private readonly string _prefix;

    public KeyedNamedSemaphore(string prefix = null,
        IEqualityComparer<TKey> keyComparer = null)
    {
        _perKey = new ConcurrentDictionary<TKey, Semaphore>(keyComparer);
        _prefix = prefix ?? $@"Global\{System.Reflection.Assembly
            .GetExecutingAssembly().GetName().Name}-";
    }

    public bool TryLock(TKey key, int maximumConcurrency, out Semaphore semaphore)
    {
        if (!_perKey.TryGetValue(key, out semaphore))
        {
            var newSemaphore = new Semaphore(maximumConcurrency, maximumConcurrency,
                $"{_prefix}{key}");
            semaphore = _perKey.GetOrAdd(key, newSemaphore);
            if (semaphore != newSemaphore) newSemaphore.Dispose();
        }
        var acquired = semaphore.WaitOne(0);
        if (!acquired) semaphore = null;
        return acquired;
    }

    public void Dispose()
    {
        foreach (var key in _perKey.Keys)
            if (_perKey.TryRemove(key, out var semaphore)) semaphore.Dispose();
    }
}

使用示例:

var locker = new KeyedNamedSemaphore<string>();
if (locker.TryLock("api/myResource/1/do-something", 10, out var semaphore))
{
    try
    {
        await DoSomethingAsync();
    }
    finally { semaphore.Release(); }
}
else
{
    await DoSomethingElseAsync();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-12-25
    • 1970-01-01
    • 1970-01-01
    • 2013-11-07
    • 2013-06-20
    • 1970-01-01
    • 2015-08-23
    • 1970-01-01
    相关资源
    最近更新 更多