第 9 个线程上的 SemaphoreSlim 死锁

SemaphoreSlim deadlocks on 9th thread

我有一个带有多线程测试的测试自动化环境,它使用共享 HttpClient 来测试我们 Web API 上的方法。 HttpClient 初始化后,我们所有的测试 运行 都可以在多线程上使用它,因为它是线程安全对象。然而,防止初始化发生不止一次是一个挑战。而且它里面包含了await关键字,所以它不能使用任何基本的锁技术来保证初始化操作是原子的。

为了确保初始化正确进行,我使用 SemaphoreSlim 创建一个用于初始化的互斥量。要访问该对象,所有测试都必须调用一个使用 SemaphoreSlim 的函数,以确保它已被第一个请求它的线程正确初始化。

我发现在 this web page 上使用 SemaphoreSlim 的以下实现。

public class TimedLock
{
    private readonly SemaphoreSlim toLock;

    public TimedLock()
    {
        toLock = new SemaphoreSlim(1, 1);
    }

    public LockReleaser Lock(TimeSpan timeout)
    {
        if (toLock.Wait(timeout))
        {
            return new LockReleaser(toLock);
        }

        throw new TimeoutException();
    }

    public struct LockReleaser : IDisposable
    {
        private readonly SemaphoreSlim toRelease;

        public LockReleaser(SemaphoreSlim toRelease)
        {
            this.toRelease = toRelease;
        }
        public void Dispose()
        {
            toRelease.Release();
        }
    }
}

我这样使用 class:

private static HttpClient _client;
private static TimedLock _timedLock = new();

protected async Task<HttpClient> GetClient()
{
    using (_timedLock.Lock(TimeSpan.FromSeconds(600)))
    {
        if (_client != null)
        {
            return _client;
        }

        MyWebApplicationFactory<Startup> factory = new();
        _client = factory.CreateClient();

        Request myRequest = new Request()
        {
            //.....Authentication code
        };

        HttpResponseMessage result = await _client.PostAsJsonAsync("api/accounts/Authenticate", myRequest);
        result.EnsureSuccessStatusCode();
        AuthenticateResponse Response = await result.Content.ReadAsAsync<AuthenticateResponse>();

        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Response.Token);
        return _client;
    }
}

直到最近我在我的代码中添加了第九个线程时,它才完美地工作。我不得不将其调回 8 个线程,因为每当我允许第 9 个线程调用 TimedLock.Lock 方法时,整个程序就会死锁。

有谁知道可能发生了什么,或者如何解决这个问题?

好的。我想出了我自己的问题,这真的是我自己的问题,不是别人的问题。

如果您将我上面的代码与我引用的源代码非常接近地进行比较,您会发现实际上存在差异。原代码使用SemaphoreSlim内置的WaitAsync函数异步实现Lock函数:

public async Task<LockReleaser> Lock(TimeSpan timeout)
{
    if(await toLock.WaitAsync(timeout))
    {
        return new LockReleaser(toLock);
    }
    throw new TimeoutException();
}

当然,在我使用它的代码中,在适当的位置添加 await 关键字以正确处理添加的任务对象:

...
using (await _timedLock.Lock(TimeSpan.FromSeconds(6000)))
{
...

昨天我“发现”如果我将 toLock 对象更改为使用 WaitAsync,问题就会神奇地消失,我为自己感到骄傲。但是就在几分钟前,当我将“原始”代码复制并粘贴到我的问题中时,我意识到“原始”代码实际上包含“我的”修复!

我现在记得几个月前看过这个,想知道为什么他们需要把它变成一个异步函数。因此,以我的超凡智慧,我在没有 Async 的情况下尝试了它,发现它运行良好,并继续进行,直到我最近才开始使用足够多的线程来证明为什么它是必要的!

因此,为了避免混淆人们,我将问题中的代码更改为我最初将其更改为的错误代码,并将真正原始的好代码放在上面的“我的”答案中,实际上应该归功于所引用文章的作者 Richard Blewett。

我不能说我完全理解为什么这个修复确实有效,所以欢迎任何可以更好地解释它的进一步答案!