SemaphoreSlim.WaitAsync 在尝试块之前/之后
Posted
技术标签:
【中文标题】SemaphoreSlim.WaitAsync 在尝试块之前/之后【英文标题】:SemaphoreSlim.WaitAsync before/after try block 【发布时间】:2014-07-31 02:28:56 【问题描述】:我知道在同步世界中,第一个 sn-p 是正确的,但是 WaitAsync 和 async/await 魔法又是什么?请给我一些 .net 内部结构。
await _semaphore.WaitAsync();
try
// todo
finally
_semaphore.Release();
或
try
await _semaphore.WaitAsync();
// todo
finally
_semaphore.Release();
【问题讨论】:
async/await
不会改变异常的行为方式inside async
方法。它只会改变propagated outside async
method 的例外情况。所以,“同步词”的正确之处仍然在这里。
这篇文章解释了在实现lock()
时考虑了同样的问题:blogs.msdn.microsoft.com/ericlippert/2009/03/06/…
【参考方案1】:
这是对 Bill Tarbell 的 LockSync
扩展方法的尝试改进,用于 SemaphoreSlim
类。通过使用值类型IDisposable
包装器和ValueTask
返回类型,可以显着减少超出SemaphoreSlim
类自身分配的额外分配。
public static ReleaseToken Lock(this SemaphoreSlim semaphore,
CancellationToken cancellationToken = default)
semaphore.Wait(cancellationToken);
return new ReleaseToken(semaphore);
public static async ValueTask<ReleaseToken> LockAsync(this SemaphoreSlim semaphore,
CancellationToken cancellationToken = default)
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
return new ReleaseToken(semaphore);
public readonly struct ReleaseToken : IDisposable
private readonly SemaphoreSlim _semaphore;
public ReleaseToken(SemaphoreSlim semaphore) => _semaphore = semaphore;
public void Dispose() => _semaphore?.Release();
使用示例(同步/异步):
using (semaphore.Lock())
DoStuff();
using (await semaphore.LockAsync())
await DoStuffAsync();
同步的Lock
始终是免分配的,无论是立即获取信号量还是在阻塞等待之后获取信号量。异步LockAsync
也是免分配的,但仅在同步获取信号量时(当它的CurrentCount
恰好是正时)。当存在争用且LockAsync
必须异步完成时,在标准SemaphoreSlim.WaitAsync
分配的基础上额外分配144 个字节(没有CancellationToken
是88 个字节,从.NET 5 开始,497 个字节带有可取消的CancellationToken
64位机)。
来自the docs:
从 C# 7.0 开始支持使用
ValueTask<TResult>
类型,并且不受任何版本的 Visual Basic 支持。
readonly
structs 从 C# 7.2 开始可用。
还解释了here 为什么IDisposable
ReleaseToken
结构没有被using
语句装箱。
【讨论】:
【参考方案2】:如果我们考虑ThreadAbortException
,这两个选项都是危险的。
-
考虑 选项 1 和
ThreadAbortException
发生在 WaitAsync
和 try
之间。在这种情况下,将获取信号量锁但从未释放。最终这会导致僵局。
await _semaphore.WaitAsync();
// ThreadAbortException happens here
try
// todo
finally
_semaphore.Release();
-
现在在选项2中,如果
ThreadAbortException
在获得锁之前发生,我们仍然会尝试释放其他人的锁,或者如果信号量是,我们会得到SemaphoreFullException
未锁定。
try
// ThreadAbortException happens here
await _semaphore.WaitAsync();
// todo
finally
_semaphore.Release();
理论上,我们可以使用 选项 2 并跟踪是否实际获得了锁。为此,我们将把锁获取和跟踪逻辑放入finally
块中的另一个(内部)try-finally
语句中。原因是ThreadAbortException
不会中断finally
块的执行。所以我们会有这样的东西:
var isTaken = false;
try
try
finally
await _semaphore.WaitAsync();
isTaken = true;
// todo
finally
if (isTaken)
_semaphore.Release();
很遗憾,我们仍然不安全。问题是Thread.Abort
会锁定调用线程,直到中止线程离开受保护区域(我们场景中的内部finally
块)。这可能会导致僵局。为了避免无限或长时间运行的信号量等待,我们可以定期中断它并给ThreadAbortException
一个中断执行的机会。现在逻辑感觉安全了。
var isTaken = false;
try
do
try
finally
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
while(!isTaken);
// todo
finally
if (isTaken)
_semaphore.Release();
【讨论】:
如果我们确定我们的代码不会调用 Thread.Abort,为什么还要考虑处理 ThreadAbortException? 您的问题非常有效,尤其是在 .NET Core 中引入的更改方面,其中Thread.Abort
被基于 CancellationToken 的更安全的方法取代。有一个很好的线程:github.com/dotnet/runtime/issues/11369 至于 .NET Framework 项目,我想说,考虑到 ThreadAbortException 仍然有效,因为很难预见它可以抛出的所有方式——包括 3rd 方库以及消费者。
只是好奇;而不是在finally
中工作的空try
块,为什么不在try
块中调用WaitAsync()
,然后使用空的catch
?
我们希望确保isTaken
变量始终使用WaitAsync()
调用的结果进行更新。 ThreadAbortException
可能发生在获取信号锁之后,但在变量更新之前。这就是为什么我们将这些操作放入finally
块中,以保证它们不会被中断。 finally
块不太可能,try-catch
可以被ThreadAbortException
中断。希望这能回答您的问题。【参考方案3】:
首选您的第一个选项,以避免在 Wait 调用抛出的情况下调用 release。不过,使用 c#8.0 我们可以编写一些东西,这样我们就不会在需要使用信号量的每个方法上出现太多难看的嵌套。
用法:
public async Task YourMethod()
using await _semaphore.LockAsync();
// todo
//the using statement will auto-release the semaphore
扩展方法如下:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace YourNamespace
public static class SemaphorSlimExtensions
public static IDisposable LockSync(this SemaphoreSlim semaphore)
if (semaphore == null)
throw new ArgumentNullException(nameof(semaphore));
var wrapper = new AutoReleaseSemaphoreWrapper(semaphore);
semaphore.Wait();
return wrapper;
public static async Task<IDisposable> LockAsync(this SemaphoreSlim semaphore)
if (semaphore == null)
throw new ArgumentNullException(nameof(semaphore));
var wrapper = new AutoReleaseSemaphoreWrapper(semaphore);
await semaphore.WaitAsync();
return wrapper;
还有 IDisposable 包装器:
using System;
using System.Threading;
namespace YourNamespace
public class AutoReleaseSemaphoreWrapper : IDisposable
private readonly SemaphoreSlim _semaphore;
public AutoReleaseSemaphoreWrapper(SemaphoreSlim semaphore )
_semaphore = semaphore;
public void Dispose()
try
_semaphore.Release();
catch
【讨论】:
您为什么要抑制_semaphore.Release();
调用的可能异常? SemaphoreFullException
的出现肯定至少应该记录下来,以便引起开发人员的注意。
因为示例中没有足够的上下文来有意义地处理异常。读者可能想简单地记录它,销毁所有使用信号量的实例,或者完全终止他们的应用程序。选择权在他们手中——这只是一个简洁的示例来展示这个概念。
比尔我的理解是AutoReleaseSemaphoreWrapper
是图书馆值得的代码。它不是一段应用程序代码,旨在根据手头应用程序的特殊需求进行定制。所以恕我直言,关于处理异常的general rules 适用:如果您不知道如何处理它,请让异常传播。【参考方案4】:
这是一个答案和一个问题的混合体。
来自一篇关于lock()
实现的文章:
这里的问题是,如果编译器在监视器进入和 try-protected 区域之间生成一个无操作指令,那么运行时可能会在监视器进入之后但在尝试之前抛出线程中止异常。在那种情况下, finally 永远不会运行,所以锁泄漏,可能最终导致程序死锁。如果这在未优化和优化的构建中是不可能的,那就太好了。 (https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/)
当然,lock
不一样,但从这个注释我们可以得出结论,如果它还提供了一种方法来确定是否锁已成功获取(如文章中所述的Monitor.Enter
)。但是,SemaphoreSlim
不提供这样的机制。
这篇关于using
实现的文章说:
using (Font font1 = new Font("Arial", 10.0f))
byte charset = font1.GdiCharSet;
转化为:
Font font1 = new Font("Arial", 10.0f);
try
byte charset = font1.GdiCharSet;
finally
if (font1 != null)
((IDisposable)font1).Dispose();
如果可以在 Monitor.Enter()
和紧随其后的 try
之间生成 noop,那么转换后的 using
代码是否也会出现同样的问题?
也许AsyncSemaphore
的这个实现
https://github.com/Microsoft/vs-threading/blob/81db9bbc559e641c2b2baf2b811d959f0c0adf24/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs
和SemaphoreSlim
的扩展名
https://github.com/StephenCleary/AsyncEx/blob/02341dbaf3df62e97c4bbaeb6d6606d345f9cda5/src/Nito.AsyncEx.Coordination/SemaphoreSlimExtensions.cs
也很有趣。
【讨论】:
【参考方案5】:根据 MSDN,SemaphoreSlim.WaitAsync
可能会抛出:
ObjectDisposedException
- 如果信号量已被释放
ArgumentOutOfRangeException
- 如果您选择接受int
的重载并且它是负数(不包括-1)
在这两种情况下,SemaphoreSlim
都不会获得锁,这使得在finally
块中释放它变得不那么简单了。
需要注意的一点是,如果在第二个示例中对象被释放或为 null,finally 块将执行并触发另一个异常或调用 Release
,它可能一开始还没有获得任何要释放的锁。
总而言之,为了与非异步锁保持一致并避免finally
块中的异常,我会选择前者
【讨论】:
Release
失败的WaitAsync
不会仍然退出信号量一次吗?
肯定会导致冗余调用。修正了我的答案【参考方案6】:
如果WaitAsync
中存在异常,则信号量未 被获取,因此Release
是不必要的,应该避免。您应该使用第一个 sn-p。
如果您担心在实际获取信号量时出现异常(除了NullReferenceException
之外不太可能),您可以尝试独立捕获它:
try
await _semaphore.WaitAsync();
catch
// handle
try
// todo
finally
_semaphore.Release();
【讨论】:
以上是关于SemaphoreSlim.WaitAsync 在尝试块之前/之后的主要内容,如果未能解决你的问题,请参考以下文章
NOIP 2015 & SDOI 2016 Round1 & CTSC 2016 & SDOI2016 Round2游记