SemaphoreSlim在第9个线程上死锁



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

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

我找到了以下在这个网页上使用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();
}
}
}

我这样使用这个类:

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方法时,整个程序就会死锁。

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

OK。我发现了我自己的问题,而且这确实是我自己的问题,与别人无关。

如果你将上面的代码与我引用的源代码进行比较,你会注意到实际上有区别。原始代码使用SemaphoreSlim中内置的WaitAsync函数异步实现Lock函数:

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

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

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

昨天我'发现';如果我将toLock对象改为使用WaitAsync,问题就神奇地消失了,我为自己感到骄傲。但是就在几分钟前,当我复制粘贴"原始"的时候代码变成了我的问题,我意识到原来的"代码实际上包含"my"修复!

我现在记得几个月前看着这个,想知道为什么他们需要把它变成一个Async函数。因此,以我的智慧,我尝试了没有Async的情况下,看到它工作得很好,并继续下去,直到我最近开始使用足够的线程来证明为什么它是必要的!

所以为了不让人们感到困惑,我把问题中的代码改成了我最初修改的坏代码,并把真正原始的好代码放在上面的"my"答案,实际上应该归功于参考文章的作者Richard Blewett。

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

最新更新