我们在 asp.net 系统上有很大的遗留问题,我们已经开始使用我们无法更改的基础结构库中的一些异步方法。 该系统在大多数地方不使用任务,但基础设施仅公开异步方法。
在代码中,我们使用以下模式来使用异步方法:
Task.Run(() => Foo()).Result
我们使用 Task.Run为了防止代码中某处有人没有使用 ConfigureAwait(false) 时出现死锁,有很多地方有人可能会错过,而且以前也发生过。 我们使用 Task.Result将其与现有的同步代码库集成。
在经历重负载之后我们注意到我们正在超时但服务器没有做任何工作(低 CPU),我们发现当对服务器和线程池有很多调用时达到最大线程数线程在 Task.Result 中达到死锁,因为它会阻塞线程直到任务完成,但任务无法运行,因为没有线程池线程可用于运行它。
最好的解决方案是将代码更改为一直异步工作,但现在还不行。 删除 Task.Run 也可以,但风险太大,因为没有足够的测试覆盖率知道我们不会在未经测试的流程中造成新的死锁。
我已经尝试实现一个新的任务调度程序,它不会使用线程池而是使用一组不同的线程来运行 Foo 任务,但是内部任务正在我不想替换的默认任务调度程序上执行.
有什么想法可以在不对代码库进行巨大更改的情况下解决这个问题吗?
这是一个重现问题的小型示例应用程序,仅使用 10 个线程而不是实际限制。在示例中,永远不会调用 Foo。
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(10, 10);
ThreadPool.SetMinThreads(10, 10);
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(CallBack);
}
Console.ReadKey();
}
private static void CallBack(object state)
{
Thread.Sleep(1000);
var result = Task.Run(() => Foo()).Result;
}
public static async Task<string> Foo()
{
await Task.Delay(100);
return "";
}
}
最佳答案
您已经很好地解释了您的问题。
当您使用 Task.Run
然后阻塞结果时,您使用的是 1-2 个线程,而当真正使用异步时您想要使用 0-1 个线程。
如果您在代码中过于随意地使用 Task.Run
,那么您将有可能拥有多层阻塞线程,使线程使用速度变得非常难看,您会遇到您所描述的最大容量。
顺便说一句,忘记尝试在单元测试(或控制台应用程序)中查找异步死锁,因为它需要作为非默认 SynchronizationContext
。
最佳和正确的解决方案是使所有内容从上到下异步或同步,但鉴于您受到限制,我建议调查 this wonderful library from the Microsoft vs team并查看 JoinableTaskFactory.Run(...)
,这将在阻塞线程上运行延续,并且当您在多个级别嵌套此模式时表现良好。使用这种方法,您将更接近于同步等效代码。
重申一下,这些技术是变通方法,如果您通过尊重现有代码来证明这些变通方法的合理性,那么尊重它的最佳方式就是正确地执行它,并使其完全同步或自上而下异步。
关于c# - 线程池死锁与 Task.Result,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44899181/