我正在重构旧的同步 C# 代码以使用异步库。当前的同步代码大量使用了锁。外部方法通常调用内部方法,两者都锁定相同的对象。这些通常是在基类中定义的“受保护对象”,并在基虚拟方法和调用基类的重写中锁定。对于同步代码,这是可以的,因为进入外部/覆盖方法锁的线程也可以进入内部/基方法锁。异步 /
SemaphoreSlim(1,1)
s 的情况并非如此。
我正在寻找一种可以在异步世界中使用的强大锁定机制,该机制将允许对同一锁定对象的后续下游调用,以按照同步“lock {...}”语法中的行为输入锁定。我最接近的是 semaphore slim,但它对我的需求来说限制太多。它不仅限制对其他线程的访问,还限制对请求进入内部调用的同一线程的访问。或者,有没有办法在调用内部
SemaphoreSlim.waitasync()
之前知道线程已经在信号量“内部”?
欢迎对锁定同一对象的内部/外部方法的设计结构提出质疑(我自己也质疑!),但如果是这样,请提出替代选项。我想过只使用私有
SemaphoreSlim(1,1)
,并让基类的继承者使用他们自己的私有信号量。但快速管理会变得很棘手。
同步示例:因为同一个线程正在请求进入内部和外部的锁,所以它允许它进入并且该方法可以完成。
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的观点:
即使在同步代码中,通常也应该避免这样的重入(它通常更容易出错)。
这是我不久前写的关于该主题的博客文章。有点啰嗦;抱歉。
我正在寻找一种可以在异步世界中使用的强大锁定机制,该机制将允许对同一锁定对象的后续下游调用,以按照同步“lock {...}”语法中的行为输入锁定。
TL;DR:没有。
更长的答案:存在一个实现,但我不会使用“健壮”这个词。
我推荐的解决方案是重构first,使代码不再依赖于锁重入。让现有代码使用
SemaphoreSlim
(带有同步 Wait
)而不是 lock
。
这种重构并不是非常简单,但我喜欢使用的一种模式是将“内部”方法重构为始终在锁定下执行的
private
(或protected
)实现方法。我强烈建议这些内部方法遵循命名约定;我倾向于使用“丑陋但在你脸上”_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);
}
如果有多个锁,这会变得更加复杂,但对于简单的情况,这种重构效果很好。然后你可以将可重入的
lock
替换为不可重入的SemaphoreSlim
:
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
编写一个返回 IDisposable
的小扩展方法,然后您最终会得到看起来与旧的 using
块更相似的 lock
块,而不是具有try
/finally
无处不在。
不推荐的解决方案:
正如canton7所怀疑的,异步递归锁是可能的,并且我已经写了一个。然而,该代码从未被发布或得到支持,也永远不会被发布或支持。它尚未在生产中得到证实,甚至还没有经过全面测试。但从技术上来说,它确实存在。