是否有一个版本的 Semaphore Slim 或其他方法可以让同一个线程进入下游?

Is there a version of Semaphore Slim or another method that will let the same thread in downstream?

我正在重构旧的同步 C# 代码以使用异步库。当前的同步代码自由使用锁。外部方法通常调用内部方法,两者都锁定相同的对象。这些通常是在基 class 中定义的“受保护对象”,并锁定在基虚拟方法和调用基的覆盖中。对于同步代码,没关系,因为进入 outer/override 方法锁的线程也可以进入 inner/base 方法一。 async / SemaphoreSlim(1,1)s 不是这种情况。

我正在寻找一种可以在异步世界中使用的强大的锁定机制,它将允许对同一锁定对象的后续下游调用进入锁定,按照同步“锁定{... }“ 句法。我最接近的是 semaphore slim,但它对我的需求来说太严格了。它不仅限制对其他线程的访问,而且也限制对请求进入内部调用的同一线程的访问。或者,有没有办法在调用内部 SemaphoreSlim.waitasync()?

之前知道线程已经在信号量“内部”

欢迎对同时锁定在同一个对象上的 inner/outer 方法的设计结构提出质疑的答案(我自己质疑!),但如果是这样,请提出替代方案。我想过只使用私有的 SemaphoreSlim(1,1)s,并让基础 class 的继承者使用他们自己的私有信号量。但要快速管理起来会很棘手。

同步示例:因为同一个线程在内部和外部都请求进入锁,它允许它进入并且该方法可以完成。

        private object LockObject = new object();
        
        public void Outer()
        {
            lock (LockObject)
            {
                foreach (var item in collection)
                {
                    Inner(item);
                }
            }

        }

        public void Inner(string item)
        {
            lock (LockObject)
            {
                DoWork(item);
            }
        }

异步示例:信号量不是那样工作的,它会卡在内部异步的第一次迭代中,因为它只是一个信号,它不会让另一个信号通过,直到它被释放,即使同一个线程请求它


        protected SemaphoreSlim LockObjectAsync = new SemaphoreSlim(1,1);

        public async Task OuterAsync()
        {
            try
            {
                await LockObjectAsync.WaitAsync();
                foreach (var item in collection)
                {
                    await InnerAsync(item);
                }
            }
            finally
            {
                LockObjectAsync.Release();
            }

        }

        public async Task InnerAsync(string item)
        {
            try
            {
                await LockObjectAsync.WaitAsync();
                DoWork(item);
            }
            finally
            {
                LockObjectAsync.Release();
            }
        }

我完全同意 Servy 的观点:

Reentrancy like this should generally be avoided even in synchronous code (it usually makes it easier to make mistakes).

Here's a blog post on the subject 前阵子写的。有点啰嗦;对不起。

I'm looking for a robust locking mechanism I can use in the async world that will allow subsequent downstream calls to the same locking object, to enter the lock, as per the behaviour in synchronous "lock {...}" syntax.

TL;DR:没有。

更长的答案:存在一个实现,但我不会使用“健壮”这个词。


我推荐的解决方案是先重构,这样代码就不再依赖于锁重入。使现有代码使用 SemaphoreSlim(同步 Waits)而不是 lock.

这种重构并不是非常简单,但我喜欢使用的一种模式是将“内部”方法重构为始终执行的 private(或 protected,如有必要)实现方法下锁。我强烈建议这些内部方法遵循命名约定;我倾向于使用 ugly-but-in-your-face _UnderLock。使用您的示例代码,它看起来像:

private object LockObject = new();
        
public void Outer()
{
  lock (LockObject)
  {
    foreach (var item in collection)
    {
      Inner_UnderLock(item);
    }
  }
}

public void Inner(string item)
{
  lock (LockObject)
  {
    Inner_UnderLock(item);
  }
}

private void Inner_UnderLock(string item)
{
  DoWork(item);
}

如果有多个锁,这会变得更加复杂,但对于简单的情况,这种重构效果很好。然后就可以把可重入的locks换成不可重入的SemaphoreSlims:

private SemaphoreSlim LockObject = new(1);
        
public void Outer()
{
  LockObject.Wait();
  try
  {
    foreach (var item in collection)
    {
      Inner_UnderLock(item);
    }
  }
  finally
  {
    LockObject.Release();
  }
}

public void Inner(string item)
{
  LockObject.Wait();
  try
  {
    Inner_UnderLock(item);
  }
  finally
  {
    LockObject.Release();
  }
}

private void Inner_UnderLock(string item)
{
  DoWork(item);
}

如果你有很多这样的方法,考虑为 SemaphoreSlim 写一个小的扩展方法 returns IDisposable,然后你最终得到 using 块看起来更像旧的 lock 块,而不是到处都是 try/finally


不推荐的方案:

正如canton7所怀疑的,异步递归锁是可能的,并且I have written one。然而,该代码从未被发布或支持,也永远不会。它尚未在生产中得到证实,甚至还没有经过全面测试。但从技术上讲,它确实存在。