如何为Parallel.ForEachAsync设置最小并行度?相关技术疑问解析
问题描述
初始代码与疑问
我有一段用于并行向一组URL发送Get请求的C#代码:
internal class Program { static async Task Main(string[] args) { string[] urls = { "https://example1.com", "https://example2.com", "https://example3.com", "https://example4.com", "https://example5.com" }; HttpClientHandler clientHandler = new HttpClientHandler(); using (var client = new HttpClient(clientHandler)) { client.Timeout = TimeSpan.FromSeconds(10); var options = new ParallelOptions { MaxDegreeOfParallelism = urls.Length }; await Parallel.ForEachAsync(urls, options, async (url, stoppingToken) => { try { Console.WriteLine("Processing " + url); HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var resp = await response.Content.ReadAsStringAsync(); Console.WriteLine("Response for " + url + " " + resp); } catch (Exception ex) { Console.WriteLine("Response for " + url + " " + ex.Message); } }); Console.WriteLine("Completed"); } Console.ReadLine(); } }
如果将MaxDegreeOfParallelism设为数组长度,这是否同时隐含了最小并行度?也就是说,代码是否会尝试生成与MaxDegreeOfParallelism数值相同数量的线程?
补充疑问(编辑1)
如果上面的代码做不到,那能不能通过以下代码实现“同时发起对应数量请求”的类似效果?
internal class Program { public async Task sendRequest(string url) { HttpClientHandler clientHandler = new HttpClientHandler(); using (var client = new HttpClient(clientHandler)) { client.Timeout = TimeSpan.FromSeconds(10); try { Console.WriteLine("Processing " + url); HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var resp = await response.Content.ReadAsStringAsync(); Console.WriteLine("Response for " + url + " " + resp); } catch (Exception ex) { Console.WriteLine("Response for " + url + " " + ex.Message); } } } static async Task Main(string[] args) { string[] urls = { "https://example1.com", "https://example2.com", "https://example3.com", "https://example4.com", "https://example5.com" }; foreach (string url in urls) { Program program = new Program(); //dont await the sendRequest() method so that you can go through whole loop Thread operation = new Thread(() => program.sendRequest(url)); operation.Start(); } Console.ReadLine(); } }
解答
关于第一个问题
MaxDegreeOfParallelism只定义了最大并行任务数,完全没有“最小并行度”的含义:
- 首先,
Parallel.ForEachAsync基于任务并行库(TPL)实现,它不会直接生成对应数量的线程。因为异步IO操作(比如GetAsync)在等待响应时会释放线程回到线程池,线程池会根据系统负载动态调度线程数量,不会为每个任务都固定分配线程。 - 其次,即使你把最大值设为数组长度,TPL也不会强制同时启动所有任务。它会根据当前线程池的可用资源、系统负载等情况,逐步启动任务,最多不超过你设定的最大值。
简单说:这个设置只是允许最多同时运行urls.Length个任务,但不会保证一定有这么多任务同时在运行,更不会生成对应数量的线程。
关于补充的代码
这段代码确实会同时启动对应数量的线程,但它存在几个严重问题:
- 资源浪费:每个线程都创建一个
HttpClient实例,HttpClient是设计为复用的对象,频繁创建和销毁会导致大量不必要的TCP连接开销,甚至可能耗尽系统端口资源。 - 异步方法误用:
sendRequest是异步方法,但你把它放到Thread里执行,相当于把异步方法当同步方法用,完全浪费了异步IO的优势——线程会一直阻塞到整个请求完成,而不是在等待响应时释放线程。 - 无统一等待机制:代码里只是启动线程就不管了,主线程靠
Console.ReadLine()阻塞,但无法知道所有请求什么时候完成,也无法处理异常的传播和汇总。
正确实现“同时发起所有请求”的方式
如果你想同时启动所有异步请求,完全不需要用Parallel.ForEachAsync或者手动创建线程,直接用Task.WhenAll即可:
internal class Program { static async Task Main(string[] args) { string[] urls = { "https://example1.com", "https://example2.com", "https://example3.com", "https://example4.com", "https://example5.com" }; // 复用同一个HttpClient实例,这是最佳实践 using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; // 创建所有请求任务 var tasks = urls.Select(async url => { try { Console.WriteLine("Processing " + url); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var resp = await response.Content.ReadAsStringAsync(); Console.WriteLine("Response for " + url + " " + resp); } catch (Exception ex) { Console.WriteLine("Response for " + url + " " + ex.Message); } }); // 等待所有任务完成 await Task.WhenAll(tasks); Console.WriteLine("Completed"); Console.ReadLine(); } }
这个方式会同时启动所有请求(因为Select会创建所有任务对象,Task.WhenAll会等待它们全部完成),而且复用了HttpClient,充分利用异步IO的优势,线程池会自动高效调度线程,不会造成资源浪费。
内容的提问来源于stack exchange,提问作者TriedAtLeast
相关产品推荐
相关产品推荐

