我们拥有asp.net系统的大量遗产,我们已经开始使用我们无法改变的基础架构库中的一些异步方法.
系统在大多数地方不使用任务,但基础结构仅公开异步方法.
在代码中,我们使用以下模式来使用异步方法:
Task.Run(()=> Foo()).结果
我们使用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(…),这将在阻塞上运行continuation线程,并在将此模式嵌套在多个级别时播放良好.使用这种方法,您将更接近同步等效代码.
重申一下,这些技术是变通方法,如果您通过尊重现有代码证明这些变通方法是正确的,那么尊重它的最佳方法就是正确执行,并使其完全同步或从上到下异步.