第 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。
我不能说我完全理解为什么这个修复确实有效,所以欢迎任何可以更好地解释它的进一步答案!
我有一个带有多线程测试的测试自动化环境,它使用共享 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。
我不能说我完全理解为什么这个修复确实有效,所以欢迎任何可以更好地解释它的进一步答案!