.net 尝试块之前/之后的信号量Slim.WaitAsync

sz81bmfz  于 2023-03-09  发布在  .NET
关注(0)|答案(6)|浏览(228)

我知道在同步世界里第一个片段是正确的,但是关于WaitAsync和async/await魔术是什么呢?请给予我一些.net的内部信息。

await _semaphore.WaitAsync();
try
{
    // todo
}
finally
{
    _semaphore.Release();
}

try
{
    await _semaphore.WaitAsync();
    // todo
}
finally
{
    _semaphore.Release();
}
vnjpjtjt

vnjpjtjt1#

根据MSDN,SemaphoreSlim.WaitAsync可能引发:

  1. ObjectDisposedException-如果已释放信号量
  2. ArgumentOutOfRangeException-如果选择接受int的重载,并且它是负数(不包括-1)
    在这两种情况下,SemaphoreSlim都不会获取锁,这使得在finally块中释放它变得不必要。
    需要注意的一点是,如果在第二个示例中对象被disposed或null,finally块将执行并触发另一个异常或调用Release,而Release可能首先没有获取任何要释放的锁。
    最后,为了与非异步锁保持一致并避免finally块中的异常,我将选择前者
vh0rcniy

vh0rcniy2#

如果我们考虑ThreadAbortException,这两个选项都是危险的,在旧的.NET Framework代码中可能会发生异常,尽管需要注意的是,它不会在新的.NET Core代码中发生,正如Microsoft所说:即使.NET Core和.NET 5+中存在此类型,但由于不支持Abort,公共语言运行库将永远不会引发ThreadAbortException。
1.考虑Option 1ThreadAbortException发生在WaitAsynctry之间。在这种情况下,将获得信号量锁但从未释放。最终将导致死锁。

await _semaphore.WaitAsync();

// ThreadAbortException happens here

try
{
    // todo
}
finally
{
    _semaphore.Release();
}

1.现在在Option 2中,如果ThreadAbortException发生在获取锁之前,我们仍然会尝试释放其他人的锁,或者如果信号量没有被锁定,我们会得到SemaphoreFullException

try
{
    // ThreadAbortException happens here

    await _semaphore.WaitAsync();
    // todo
}
finally
{
    _semaphore.Release();
}

理论上,我们可以使用Option 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();
    }
}
sshcrbum

sshcrbum3#

如果WaitAsync内部有异常,则信号量是notacquired,因此Release是不必要的,应该避免,应该使用第一个代码片段。
如果您担心在实际获取信号量时出现异常(除了NullReferenceException之外,不太可能出现异常),您可以尝试单独捕获它:

try
{
    await _semaphore.WaitAsync();
}
catch
{
    // handle
}

try
{
    // todo
}
finally
{
    _semaphore.Release();
}
luaexgnf

luaexgnf4#

第一个选项是为了避免在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 Package 器:

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 { }
    }
  }
}
q35jwt9p

q35jwt9p5#

这是对Bill Tarbell的LockSync类的SemaphoreSlim扩展方法的一个尝试性改进。通过使用值类型IDisposable Package 器和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个字节(在64位机器上,从.NET 5开始,不带CancellationToken时为88个字节,带可取消CancellationToken时为497个字节)。
来自文档:
从C# 7.0开始支持使用ValueTask<TResult>类型,任何版本的Visual Basic都不支持。
readonlystructs从C# 7.2开始可用。
此外,还解释了here为什么IDisposableReleaseToken结构没有被using语句装箱。

ghhkc1vu

ghhkc1vu6#

这是一个答案和问题的混合体。
来自一篇关于lock(){}实现的文章:
这里的问题是,如果编译器在监视器输入和try保护区域之间生成了一个no-op指令,那么运行时就有可能在监视器输入之后、try之前抛出一个线程中止异常。在这种情况下,finally永远不会运行,所以锁会泄漏。可能最终导致程序死锁。如果这在未优化和优化的版本中都不可能,那就太好了。(https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/
当然,lock是不同的,但是从这篇注解中我们可以得出结论,如果SemaphoreSlim.WaitAsync()还提供了一种方法来确定锁是否被成功获取(就像本文中描述的Monitor.Enter那样),那么将SemaphoreSlim.WaitAsync()放在try块中可能也会更好,但是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();
  }
}

如果noop可以在Monitor.Enter()和紧随其后的try之间生成,那么同样的问题是否也适用于转换后的using代码?
也许这个AsyncSemaphorehttps://github.com/Microsoft/vs-threading/blob/81db9bbc559e641c2b2baf2b811d959f0c0adf24/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs的实现
以及SemaphoreSlimhttps://github.com/StephenCleary/AsyncEx/blob/02341dbaf3df62e97c4bbaeb6d6606d345f9cda5/src/Nito.AsyncEx.Coordination/SemaphoreSlimExtensions.cs的扩展名
也很有趣。

相关问题