使用WebClient DownloadStringAsync时抛出TargetInvocationException的解决办法
修复WebClient异步下载网页的TargetInvocationException问题及优化方案
异常原因分析
TargetInvocationException是.NET包装异步操作内部异常的容器,你的代码里空catch {}块吃掉了所有异常,导致看不到真正的错误根源——可能是无效URL、网络连接失败、服务器返回HTTP错误(如404/500)、编码解析错误等。
同时你的代码存在几个核心逻辑问题,不仅会导致异常,还会让下载结果完全不符合预期:
- 方法在所有异步下载任务完成前就返回了
HashSet,调用者拿到的会是空集合 - 循环中捕获
i的引用,可能导致多个回调使用同一个i值(闭包捕获的是变量本身,不是迭代的当前值) - 每个请求新建
WebClient,资源利用率低,且没有妥善释放资源 HashSet.ElementAt(i)性能极差,HashSet是无序集合,不支持高效的索引访问
修复方案(推荐使用HttpClient)
WebClient是.NET Framework时代的旧API,推荐使用HttpClient(支持连接池复用、异步编程更友好),以下是修复后的异步代码:
public async Task<HashSet<string>> DownloadWebpagesAsync(HashSet<string> urls) { var htmlSet = new HashSet<string>(); // 复用HttpClient,避免频繁创建释放连接 using var httpClient = new HttpClient(); // 收集所有下载任务 var downloadTasks = urls.Select(async url => { try { // 异步获取网页内容 var htmlContent = await httpClient.GetStringAsync(url); // 线程安全地添加到集合 lock (htmlSet) { htmlSet.Add(htmlContent); } } catch (Exception ex) { // 记录错误,方便排查具体URL的问题 Console.WriteLine($"下载URL [{url}] 失败: {ex.Message}"); } }); // 等待所有下载任务完成 await Task.WhenAll(downloadTasks); return htmlSet; }
方案优势
- 用
async/await替代回调,异步流程更清晰,避免回调地狱 - 复用
HttpClient,通过连接池提升下载效率 - 等待所有任务完成后再返回集合,确保结果完整
- 捕获并记录具体异常,能快速定位哪个URL出了问题
- 遍历
HashSet时直接用Select,避免低效的ElementAt操作
可选:限制并发数(避免过载)
如果URL数量较多,建议限制同时发起的请求数,防止触发服务器限流或本地网络过载,添加SemaphoreSlim实现并发控制:
public async Task<HashSet<string>> DownloadWebpagesAsync(HashSet<string> urls, int maxConcurrency = 5) { var htmlSet = new HashSet<string>(); using var httpClient = new HttpClient(); // 限制最大并发请求数 using var semaphore = new SemaphoreSlim(maxConcurrency); var downloadTasks = urls.Select(async url => { await semaphore.WaitAsync(); try { var htmlContent = await httpClient.GetStringAsync(url); lock (htmlSet) { htmlSet.Add(htmlContent); } } catch (Exception ex) { Console.WriteLine($"下载URL [{url}] 失败: {ex.Message}"); } finally { // 释放信号量,允许其他请求执行 semaphore.Release(); } }); await Task.WhenAll(downloadTasks); return htmlSet; }
若必须使用WebClient的修复版本
如果项目限制必须用WebClient,可以改用DownloadStringTaskAsync配合async/await,避免回调的闭包问题:
public async Task<HashSet<string>> DownloadWebpagesAsync(HashSet<string> urls) { var htmlSet = new HashSet<string>(); var taskList = new List<Task>(); foreach (var url in urls) { using var client = new WebClient(); // 异步获取任务,避免回调的闭包陷阱 var downloadTask = client.DownloadStringTaskAsync(new Uri(url)) .ContinueWith(task => { try { if (task.IsCompletedSuccessfully) { lock (htmlSet) { htmlSet.Add(task.Result); } } else if (task.Exception != null) { // 取出内部异常(TargetInvocationException的InnerException才是真实错误) var realException = task.Exception.InnerException ?? task.Exception; Console.WriteLine($"下载URL [{url}] 失败: {realException.Message}"); } } catch (Exception ex) { Console.WriteLine($"处理URL [{url}] 结果失败: {ex.Message}"); } }); taskList.Add(downloadTask); } await Task.WhenAll(taskList); return htmlSet; }
使用说明
调用上述方法时,必须用await等待异步操作完成:
var urls = new HashSet<string> { "https://example.com", "https://google.com" }; var result = await DownloadWebpagesAsync(urls);
内容的提问来源于stack exchange,提问作者Darren Peterson
相关产品推荐
相关产品推荐

