如何让C#并行循环中的异步方法正确运行?含WinForm适配问题
Parallel.ForEach异步下载失效及WinForm运行异常的解决方法
问题说明
- 控制台程序中,使用
Parallel.ForEach调用同步下载函数可正常运行,但异步版本无法完成下载 - WinForm程序中,同步和异步版本均无法正常运行,需明确原因并给出正确实现
原代码
主程序代码
using DLWeb; using System.Collections.Concurrent; // 初始化测试数据 ConcurrentBag<Website> _websites = [ new Website("BBC", "https://www.bbc.co.uk"), new Website("Google", "https://www.google.com"), new Website("Amazon","https://www.amazon.com")]; // 设置运行模式 bool USE_DOWNLOAD_ASYNC = true; string mode = USE_DOWNLOAD_ASYNC ? "异步" : "同步"; Console.WriteLine($"开始执行({mode}下载模式)...{Environment.NewLine}"); ParallelLoopResult _loopRes = new(); if (USE_DOWNLOAD_ASYNC) // 异步分支 { await Task.Run(() => { _loopRes = Parallel.ForEach<Website>(_websites, async (item) => { var res = await DownloadAsync(item); item.Content = res; }); }); } else // 同步分支 { await Task.Run(() => { _loopRes = Parallel.ForEach<Website>(_websites, (item) => { string s = ""; s = Download(item.URL); item.Content = s; }); }); } if (_loopRes.IsCompleted) { foreach (var item in _websites) { Console.WriteLine($"{item.Name}: 内容长度 {item.Content.Length}{Environment.NewLine}"); } Console.WriteLine("执行完成。"); } Console.ReadKey(); // ---------------------------------------------------- static string Download(string site) { HttpClient httpClient = new(); var r = httpClient.GetStringAsync(site).Result; return r; } async static Task<string> DownloadAsync(Website item) { HttpClient httpClient = new(); string res; res = "" + await httpClient.GetStringAsync(item.URL); return res; }
Website类代码
public class Website { public string Name { get; set; } = ""; public string URL { get; set; } = ""; public string Content { get; set; } = ""; public Website() { } public Website(string name, string url) { Name = name; URL = url; } }
问题分析
1. 异步版本失效的核心原因
Parallel.ForEach是基于线程的并行工具,不支持异步委托。传入async (item) => ...这类lambda时,会被当作async void方法处理——Parallel无法感知异步操作的完成,会直接判定当前迭代结束,导致_loopRes.IsCompleted提前返回true,但实际下载任务仍在后台运行,最终Content可能为空或不完整。
2. WinForm中同步/异步版本均失效的原因
- 同步版本死锁:WinForm的UI线程有专属同步上下文,调用
httpClient.GetStringAsync(site).Result时,UI线程会阻塞等待异步操作完成;而异步操作结束后需要回到UI线程继续执行,此时UI线程已被阻塞,形成死锁,程序彻底卡死。 - 异步版本双重错误:既存在
Parallel.ForEach不支持异步委托的问题,又因在WinForm中用Task.Run包裹并行逻辑,虽避免了UI线程阻塞,但Parallel依然无法等待异步任务完成,结果仍不正确。
3. HttpClient的不当使用
每次下载都创建新的HttpClient实例会导致系统Socket资源耗尽,正确做法是复用单个HttpClient实例(或使用IHttpClientFactory)。
正确实现方案
控制台/WinForm通用的异步并行下载代码
抛弃Parallel.ForEach,改用Task.WhenAll实现异步并行——这是异步场景下的标准并行方式:
using System.Collections.Concurrent; // 初始化测试数据 ConcurrentBag<Website> _websites = [ new Website("BBC", "https://www.bbc.co.uk"), new Website("Google", "https://www.google.com"), new Website("Amazon","https://www.amazon.com")]; // 复用HttpClient实例(关键优化) using HttpClient httpClient = new(); Console.WriteLine("开始执行异步下载..."); // 生成所有下载任务 var downloadTasks = _websites.Select(async item => { item.Content = await httpClient.GetStringAsync(item.URL); }); // 等待所有任务完成 await Task.WhenAll(downloadTasks); // 输出结果 foreach (var item in _websites) { Console.WriteLine($"{item.Name}: 内容长度 {item.Content.Length}{Environment.NewLine}"); } Console.WriteLine("执行完成。"); // WinForm中不需要Console.ReadKey,直接更新UI即可
WinForm中的正确调用方式
在WinForm的事件处理方法中,直接使用async/await,避免阻塞UI线程:
private async void btnDownload_Click(object sender, EventArgs e) { btnDownload.Enabled = false; try { ConcurrentBag<Website> _websites = [ new Website("BBC", "https://www.bbc.co.uk"), new Website("Google", "https://www.google.com"), new Website("Amazon","https://www.amazon.com")]; using HttpClient httpClient = new(); var downloadTasks = _websites.Select(async item => { item.Content = await httpClient.GetStringAsync(item.URL); // 实时更新UI需切换回UI线程 Invoke(() => { listBoxResults.Items.Add($"{item.Name}: 下载中..."); }); }); await Task.WhenAll(downloadTasks); listBoxResults.Items.Clear(); foreach (var item in _websites) { listBoxResults.Items.Add($"{item.Name}: 内容长度 {item.Content.Length}"); } } catch (Exception ex) { MessageBox.Show($"下载出错:{ex.Message}"); } finally { btnDownload.Enabled = true; } }
关键总结
- 异步并行场景不要用Parallel.ForEach,改用
Task.WhenAll,这是异步编程模型的最佳实践 - WinForm中绝对避免在UI线程调用
.Result/.Wait(),会导致死锁,全程用async/await - 必须复用
HttpClient实例,避免Socket资源泄漏
内容的提问来源于stack exchange,提问作者D.Man
相关产品推荐
相关产品推荐

