You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 18:14:57