这是一个场景。由 IIS 托管的网关。一个请求可能需要向另一个服务发出多个独立的其他请求,每个请求可能需要几秒钟。自然地,我认为它是并行化的一个很好的候选者。但是,如果我应该这样做,我有一个内部斗争,如果我这样做,那么我应该使用什么样的并行度。我担心的是应用程序中的所有线程通常都由 .net 线程池管理,所以如果我最终分配了太多线程(甚至等待),我可能最终会窒息其他网关功能和 API 吗?
2 回答
PLINQ对于不是您的情况的计算绑定操作很有用。对您来说有意义的是异步 I/O。
在 .NET 中进行异步 I/O 有多种方法:普通的旧APM、TPL、Reactive Extensions、C# 5 async/await、F# async workflows。后三种技术允许您以各种方式组合多个 I/O 操作并减少样板代码的数量。
异步 I/O 不是以任何方式分配新线程。它使用I/O 完成端口来避免等待回复时的线程阻塞。
我应该强调这一点:当您在已经并发的环境 (IIS) 中创建多个长时间运行的请求时,使用异步 I/O 您肯定会获得更好的吞吐量和线程池利用率。
考虑以下代码:
Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));
当它用于客户端程序时,它可以让你不阻塞 UI 线程,同时不费心在内部进行DoSomething
异步 I/O。
当您已经拥有并发服务并DoSomething
同步执行 I/O 时,您最终会在繁忙的池中使用额外的阻塞线程。如果您对每个传入请求并行执行 3 个传出请求,则 10 个同时传入的请求将阻塞池中的 30 个线程。这不是非常可扩展的方法。
当你有 I/O 绑定任务时,你的任务是否安排在新线程上并不重要,重要的是这个任务是否阻塞线程。
因此,使用 TPL,我看到以下模式比上面的代码更好:
var tasks = new Task<System.Net.WebResponse>[2];
var urls = new string[] { "https://stackoverflow.com/", "http://google.com/" };
for (int i = 0; i < tasks.Length; i++)
{
var wr = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(urls[i]);
tasks[i] = Task.Factory.FromAsync<System.Net.WebResponse>(
wr.BeginGetResponse,
wr.EndGetResponse,
null);
}
var result = Task.Factory.ContinueWhenAll(
tasks,
results => {
foreach (var result in results)
{
var resp = result.Result.GetResponseStream();
// Process responses and combine them into single result
}
});
同样,上面 90% 的代码都是样板代码。关键特性是 WebRequest 的BeginGetResponse
异步操作,无论您最终使用哪种高级技术(TPL、Rx 等),都应该使用它。
您的担忧在一定程度上是有效的。确实,线程池可能不堪重负。这就是为什么调用长时间运行的非计算 IO 函数的网关服务通常更喜欢异步的原因。异步并不意味着“一劳永逸”。尽管这种等待不再阻塞线程,但您仍然可以“等待”。
这需要大量的代码更改。
一个更简单的解决方案是将负载生成器放入您的服务并测量它是否是一个问题。如果是的话,一个快速的解决方法是大大增加线程池的大小(我有 500-1000 个线程的积极经验(默认 = 250))。这对于吞吐量来说不是最佳的,但它会起作用、可靠并且需要很少的开发时间。只要确保在负载下进行测试,这样您就不会在晚上收到任何讨厌的寻呼机呼叫。
请注意,尽管异步现在风靡一时,但在开发人员的生产力方面它也有其缺点。有时,产生 100 条线程的好旧解决方案就足够了,实际上是最好的工程权衡。如果您使用的是 C# 5.0,则可以更轻松地利用async/await
和实现非阻塞 IO。