为什么Task.Run(async () => await StringGetAsync()).Wait() 会减少StackExchange.Redis的超时异常?
在负载测试期间,我在从Redis读取一个纪元值(一个简单的整数)时遇到了大量的Redis超时异常。该应用是一个同步实现的.NET Core Web API项目,值的检索使用StackExchange.Redis的 database.StringGet()。为了模拟其他工作,我在代码中加入了一个2 秒的Thread.Sleep()。
该负载测试在我的本地开发环境中大致模拟了5 万次请求。在这样的负载下,我遇到了大量Redis超时。我的初始假设是,这种行为是可以预期的,很可能是由线程池枯竭引起的,因为同步的Redis调用会阻塞请求线程直到I/O完成。
我理解将请求流程完全改为异步执行——从控制器层到Redis访问层——很可能会降低或消除这些超时问题。
然而,出于实验目的,我并未将控制器改为异步,而是将同步的Redis调用替换为 database.StringGetAsync(),并将其包裹在 Task.Run(...).Wait() 中。我还在 Task.Run() 内部引入了一个人工的 Task.Delay(),以模拟额外的繁重处理。此前的Thread.Sleep() 已被移除。
概念上,我预计这两种方法(StringGet() 和 Task.Run(...).Wait())的行为应该类似,因为它们最终都会阻塞执行,且仍然会导致线程枯竭和Redis超时。
令人惊讶的是,在相同负载条件下,Task.Run(...).Wait() 的实现并未出现Redis超时。
我进行了广泛的调查,并使用了多种AI工具,但我无法找到一个令人信服的解释,解释为何将异步的Redis操作包裹在 Task.Run().Wait() 内部的行为,在高负载下与直接同步的 StringGet() 调用表现不同。
public string GetEpochSync(string key)
{
var value = _database.StringGet(key);
Thread.Sleep(2000);
return value;
}
// Async version
public async Task<string> GetEpochAsync(string key)
{
var value = await _database.StringGetAsync(key);
await Task.Delay(2000);
return value;
}
// Sync wrapper with toggle
public string GetEpoch(string key, bool useAsync)
{
if (useAsync)
{
// Run async inside sync
return Task.Run(async () =>
{
return await GetEpochAsync(key);
}).GetAwaiter().GetResult();
}
return GetEpochSync(key);
}
解决方案
问题在于你的线程本来就是一个异步任务。 (我只是从字里行间推断出来的;你是在一个Web服务方法中尝试的。别自责。我不久前也犯过同样的错。)
如果你处于接近最大线程池负载的状态,并且在其中尝试调用Thread.Sleep(),线程池可能会卡住,且需要很长时间来创建更多线程以扩充线程池。
如果你必须这样做,如我一样,需要考虑以更高的初始线程数来创建线程池。你可以在Program.cs中可靠地进行调整。
Task.Run() 看起来能缓解问题的原因是,调用Task.Run() 让线程池更早地注意到你正在把它塞满。