尝试阻止之前/之后的SemaphoreSlim.WaitAsync

时间:2014-06-10 10:48:03

标签: c# .net locking async-await semaphore

我知道在同步世界中第一个片段是正确的,但是有关WaitAsync和async / await magic的内容是什么?请给我一些.net内部。

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

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

5 个答案:

答案 0 :(得分:11)

根据MSDN,SemaphoreSlim.WaitAsync可能会抛出:

  1. ObjectDisposedException - 如果信号量已被处理

  2. ArgumentOutOfRangeException - 如果您选择接受int的重载并且它是负数(不包括-1)

  3. 在这两种情况下,SemaphoreSlim都不会获得锁定,这使得在finally块中释放它会让人感到不安。

    需要注意的一件事是,如果对象在第二个示例中处理或为null,则finally块将执行并触发另一个异常或调用可能尚未获取任何锁定的Release

    总而言之,我会选择前者与非异步锁定保持一致并避免finally块中的异常

答案 1 :(得分:6)

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

  1. 考虑选项1 ThreadAbortException发生在WaitAsynctry之间。在这种情况下,将获得信号量锁,但永远不会释放它。最终将导致僵局。
await _semaphore.WaitAsync();

// ThreadAbortException happens here

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

  1. 现在在选项2 中,如果ThreadAbortException发生在获得锁之前,我们仍然会尝试释放别人的锁,否则我们会得到SemaphoreFullException如果信号量未锁定。
try
{
    // ThreadAbortException happens here

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

从理论上讲,我们可以使用选项2 并跟踪是否实际获得了锁。为此,我们将在try-finally块中将锁获取和跟踪逻辑放入另一个(内部)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();
    }
}

答案 2 :(得分:2)

如果WaitAsync内有异常,则信号量 获取,所以Release是不必要的,应该避免。你应该使用第一个片段。

如果您担心实际获取信号量时出现异常(除了NullReferenceException之外不可能),您可以尝试独立捕获它:

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

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

答案 3 :(得分:1)

首选使用您的第一个选项,以避免在发生Wait调用的情况下调用释放。不过,使用 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 { }
    }
  }
}

答案 4 :(得分:0)

这是答案和问题的混合。

来自关于lock(){}实施的文章:

  

这里的问题是,如果编译器在监视器输入和try-protected区域之间生成无操作指令,那么运行时可能会在监视器输入之后但在尝试之前抛出线程中止异常。在那种情况下,最终永远不会运行,因此锁泄漏,可能最终导致程序死锁。如果在未优化和优化的构建中这是不可能的,那将是很好的。   (https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/

当然,lock不一样,但从这个说明中我们可以得出结论,将SemaphoreSlim.WaitAsync()置于try块中也可能更好,如果它也是提供了一种方法来确定锁是否已成功获取(如文章中所述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

也很有趣。