如何快速下载千余文件?优化C#下载代码兼顾速度与资源占用
优化文件下载速度:低资源占用的多线程方案
嘿,我懂你的需求——想加快图片/MP4文件的下载速度,但又不想过度占用CPU、带宽这些系统资源,毕竟没人想让电脑卡到没法用对吧?咱们来一步步优化你的现有代码,找到平衡速度和资源占用的最优方案。
首先得说:你原来用WebClient单线程循环下载的方式,每次只能处理一个URL,速度慢是必然的。而且WebClient现在算是比较旧的API了,更推荐用HttpClient——它能复用连接池,性能更好,也更适合多场景的下载需求。
下面给你两个实用的方案,都是可控并发的思路(核心就是限制同时下载的数量,避免资源过载):
方案一:异步+SemaphoreSlim(推荐,低资源占用)
这个方案用异步IO操作配合信号量精准控制并发数,线程利用率高,CPU占用极低(因为大部分时间都在等网络响应,不会阻塞线程),非常适合IO密集型的下载场景。
// 复用HttpClient实例,不要在循环里创建! private static readonly HttpClient _httpClient = new HttpClient(); private static SemaphoreSlim _downloadSemaphore; /// <summary> /// 异步批量下载文件,可控制最大并发数 /// </summary> /// <param name="saveLocation">文件保存目录</param> /// <param name="urls">待下载的URL列表</param> /// <param name="maxConcurrentDownloads">最大同时下载数,默认3,可根据带宽调整</param> public static async Task DownloadAsync(string saveLocation, List<string> urls, int maxConcurrentDownloads = 3) { // 初始化信号量,限制并发数 _downloadSemaphore = new SemaphoreSlim(maxConcurrentDownloads); var downloadTasks = new List<Task>(); // 确保保存目录存在 Directory.CreateDirectory(saveLocation); foreach (var url in urls) { // 等待信号量,获取下载权限 await _downloadSemaphore.WaitAsync(); // 添加下载任务到列表 downloadTasks.Add(DownloadSingleFileAsync(url, saveLocation)); } // 等待所有下载任务完成 await Task.WhenAll(downloadTasks); } private static async Task DownloadSingleFileAsync(string url, string saveLocation) { try { // 从URL中提取文件名 var fileName = Path.GetFileName(url); var savePath = Path.Combine(saveLocation, fileName); // 异步获取文件流,异步写入本地文件 using (var responseStream = await _httpClient.GetStreamAsync(url)) using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, useAsync: true)) { await responseStream.CopyToAsync(fileStream); } Console.WriteLine($"✅ 下载完成: {fileName}"); } catch (Exception ex) { Console.WriteLine($"❌ 下载失败 {url}: {ex.Message}"); } finally { // 释放信号量,让其他任务可以开始下载 _downloadSemaphore.Release(); } }
这个方案的优点:
- 资源占用低:异步IO不会阻塞线程,CPU使用率不会飙升
- 精准控并发:通过
maxConcurrentDownloads可以灵活调整同时下载的数量(比如从2开始测试,慢慢调到不影响系统流畅的最大值) - 性能更优:复用
HttpClient实例,避免频繁创建销毁连接池资源,减少网络开销
方案二:Parallel.ForEach(简单易改)
如果你不想改成异步方法,或者希望代码改动最小,这个方案更适合。它用Parallel.ForEach实现并行下载,同时通过MaxDegreeOfParallelism限制并发数。
private static readonly HttpClient _httpClient = new HttpClient(); /// <summary> /// 并行批量下载文件,控制最大并行数 /// </summary> /// <param name="saveLocation">文件保存目录</param> /// <param name="urls">待下载的URL列表</param> /// <param name="maxDegreeOfParallelism">最大并行数,默认3</param> public static void DownloadParallel(string saveLocation, List<string> urls, int maxDegreeOfParallelism = 3) { Directory.CreateDirectory(saveLocation); // 设置并行选项,限制最大并行数 var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }; Parallel.ForEach(urls, parallelOptions, url => { try { var fileName = Path.GetFileName(url); var savePath = Path.Combine(saveLocation, fileName); // 同步获取流并写入文件(因为Parallel.ForEach不直接支持异步lambda) using (var responseStream = _httpClient.GetStreamAsync(url).Result) using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) { responseStream.CopyTo(fileStream); } Console.WriteLine($"✅ 下载完成: {fileName}"); } catch (Exception ex) { Console.WriteLine($"❌ 下载失败 {url}: {ex.Message}"); } }); }
这个方案的特点:
- 代码改动小:和你原来的循环结构类似,容易上手
- 快速见效:比单线程下载快很多,同时不会无限制占用资源
- 注意:这里用了
.Result同步等待网络请求,相比异步方案,线程占用会稍高一些,但对于你的需求来说完全够用
一些关键注意事项
- 一定要复用HttpClient:绝对不要在循环里创建
HttpClient实例,否则会耗尽系统Socket资源,导致下载失败或报错 - 调整并发数:根据你的带宽和设备性能调整
maxConcurrentDownloads或maxDegreeOfParallelism的值——比如带宽小就设2-3,带宽大可以调到5-8,找到最适合你的平衡点 - 异常处理:代码里已经加了基础的异常捕获,你可以根据需求扩展(比如重试机制、日志记录)
- 超时设置:可以给
HttpClient加超时限制,避免某个下载任务一直卡住:_httpClient.Timeout = TimeSpan.FromMinutes(5);
内容的提问来源于stack exchange,提问作者user5405648
相关产品推荐
相关产品推荐

