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&lt;TResult&gt; 类型,并且不受任何版本的 Visual Basic 支持。

readonly structs 从 C# 7.2 开始可用。

还解释了here 为什么IDisposable ReleaseToken 结构没有被using 语句装箱。

【讨论】:

【参考方案2】:

如果我们考虑ThreadAbortException,这两个选项都是危险的。

    考虑 选项 1ThreadAbortException 发生在 WaitAsynctry 之间。在这种情况下,将获取信号量锁但从未释放。最终这会导致僵局。
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游记

秋的潇洒在啥?在啥在啥?

上传的数据在云端的怎么查看,保存在啥位置?

在 React 应用程序中在哪里转换数据 - 在 Express 中还是在前端使用 React?

存储在 plist 中的数据在模拟器中有效,但在设备中无效