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

sh1*_*1ng 7 .net c# semaphore locking async-await

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

await _semaphore.WaitAsync();
try
{
    // todo
}
finally
{
    _semaphore.Release();
}
Run Code Online (Sandbox Code Playgroud)

要么

try
{
    await _semaphore.WaitAsync();
    // todo
    }
    finally
    {
        _semaphore.Release();
    }
}
Run Code Online (Sandbox Code Playgroud)

Yuv*_*kov 13

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

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

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

在这两种情况下,都SemaphoreSlim不会获得锁定,这使得将其释放到一个finally区块中是不明智的.

需要注意的一件事是,如果对象在第二个示例中被处理或为空,则finally块将执行并触发另一个异常或调用Release,这可能没有获得任何锁定以在第一个位置释放.

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


i3a*_*non 9

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

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

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

try
{
    // todo
}
finally
{
    _semaphore.Release();
}
Run Code Online (Sandbox Code Playgroud)


And*_*yCh 7

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

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

// ThreadAbortException happens here

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

Run Code Online (Sandbox Code Playgroud)
  1. 现在在选项 2 中,如果ThreadAbortException发生在获得锁之前,我们仍然会尝试释放其他人的锁,或者SemaphoreFullException如果信号量没有锁定我们就会释放。
try
{
    // ThreadAbortException happens here

    await _semaphore.WaitAsync();
    // todo
}
finally
{
    _semaphore.Release();
}
Run Code Online (Sandbox Code Playgroud)

理论上,我们可以使用选项 2并跟踪是否实际获取了锁。为此,我们将把锁获取和跟踪逻辑放到try-finally一个finally块中的另一个(内部)语句中。原因是ThreadAbortException它不会中断finally块的执行。所以我们会有这样的事情:

var isTaken = false;

try
{
    try
    {           
    }
    finally
    {
        await _semaphore.WaitAsync();
        isTaken = true;
    }

    // todo
}
finally
{
    if (isTaken)
    {
        _semaphore.Release();
    }
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,我们仍然不安全。问题是它会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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果我们确定我们的代码不会调用 Thread.Abort,为什么我们要考虑处理 ThreadAbortException? (5认同)
  • 您的问题非常有效,特别是在 .NET Core 中引入的更改方面,其中“Thread.Abort”被基于 CancellationToken 的更安全方法取代。关于这一点,有一个很好的线程:https://github.com/dotnet/runtime/issues/11369 至于 .NET Framework 项目,我想说,考虑到 ThreadAbortException 仍然有效,因为很难预见它的所有方式可以抛出 - 包括第三方库和消费者。 (3认同)

The*_*ias 5

This is an attempted improvement of Bill Tarbell's LockSync extension method for the SemaphoreSlim class. By using a value-type IDisposable wrapper and a ValueTask return type, it is possible to reduce significantly the additional allocations beyond what the SemaphoreSlim class allocates by itself.

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();
}
Run Code Online (Sandbox Code Playgroud)

Usage example (sync/async):

using (semaphore.Lock())
{
    DoStuff();
}

using (await semaphore.LockAsync())
{
    await DoStuffAsync();
}
Run Code Online (Sandbox Code Playgroud)

同步Lock始终是免分配的,无论信号量是立即获取还是在阻塞等待之后获取。异步LockAsync也是免分配的,但仅当同步获取信号量时(当时恰好CurrentCount为正)。当存在争用且LockAsync必须异步完成时,将在标准分配之外额外分配 144 字节(在 64 位计算机上,SemaphoreSlim.WaitAsync不带 时为 88 字节CancellationToken,从 .NET 5 开始可取消为 497 字节)。CancellationToken

来自文档

从 C# 7.0 开始支持使用该ValueTask<TResult>类型,但任何版本的 Visual Basic 均不支持该类型。

readonly 从 C# 7.2 开始,结构体可用。

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

注意:就我个人而言,我不喜欢(错误地)将该using语句用于释放非托管资源以外的目的。