C#使用WebClient下载MP4触发WebException:文件被其他进程占用
看起来你遇到的这个问题很典型——只有MP4文件触发“被占用”错误,而且是首次下载,大概率是系统级进程抢先锁定了刚下载(或正在下载)的MP4文件,而非你的代码本身重复占用。下面我拆解下原因和对应的解决方案:
核心原因分析
MP4是媒体文件,Windows系统自带的媒体预览服务(比如Windows Explorer的缩略图生成)、Windows Search索引器,或者第三方杀毒软件实时扫描,会在文件开始写入磁盘后立刻尝试读取文件内容,这时候WebClient还没完全释放文件句柄,就会触发“被占用”的IO异常。而Parallel.ForEach的高并行度(20个线程)会放大这个问题——同时下载的MP4越多,系统进程争抢文件句柄的概率就越高。
解决方案
1. 先降低并行度试试(快速验证)
把MaxDegreeOfParallelism从20降到5-10,减少同时下载的MP4数量,给系统进程足够的时间处理单个文件,避免扎堆争抢:
Parallel.ForEach( files, new ParallelOptions { MaxDegreeOfParallelism = 8 }, // 调低并行数 currentFile => { DownloadFile(currentFile); } );
2. 下载到临时文件,完成后再重命名(最可靠的方案)
通过“临时文件中转”的方式,绕开系统进程对目标文件的锁定。因为同一磁盘卷下的文件重命名是原子操作,几乎不会被打断:
private static void DownloadFile(AFile file) { string tempPath = null; try { // 生成唯一临时文件名 tempPath = Path.Combine(Path.GetDirectoryName(file.SaveLocation), $"temp_{Guid.NewGuid()}.tmp"); using (var webClient = new WebClient()) { webClient.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0)"); // 先下载到临时文件 webClient.DownloadFile(new Uri(file.Address), tempPath); } // 确保目标文件不存在后,重命名临时文件为最终文件名 if (File.Exists(file.SaveLocation)) { File.Delete(file.SaveLocation); } File.Move(tempPath, file.SaveLocation); } catch (WebException e) { Logger.Warn(e.ToString()); FailedDownloadCount++; } finally { // 无论成功失败,清理临时文件 if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) { try { File.Delete(tempPath); } catch { /* 忽略清理失败的情况 */ } } System.Threading.Thread.Sleep(250); } }
3. 排查并排除系统进程的干扰
如果想精准定位是哪个进程在占用文件,可以用Process Explorer工具:
- 当报错时,立刻搜索目标MP4文件的路径,查看哪个进程持有该文件的句柄。
- 如果是Windows预览服务,可以临时关闭
Windows Search服务,或者把下载目录添加到索引排除列表;如果是杀毒软件,把下载目录加入实时扫描的白名单。
4. 改用HttpClient替代WebClient(可选优化)
WebClient是较旧的API,HttpClient在资源管理和异步支持上更完善。如果你的项目是.NET 6+,可以用Parallel.ForEachAsync配合HttpClient实现异步并行下载,资源释放更及时:
public static class DownloadUtilities { public static int FailedDownloadCount; // 复用HttpClient实例,避免频繁创建连接 private static readonly HttpClient _httpClient = new HttpClient(); static DownloadUtilities() { _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/4.0 (compatible; MSIE 8.0)"); } public static async Task DownloadLinks(List<AFile> files) { FailedDownloadCount = 0; await Parallel.ForEachAsync(files, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async (currentFile, ct) => { await DownloadFileAsync(currentFile, ct); } ); } private static async Task DownloadFileAsync(AFile file, CancellationToken ct) { string tempPath = Path.Combine(Path.GetDirectoryName(file.SaveLocation), $"temp_{Guid.NewGuid()}.tmp"); try { using (var response = await _httpClient.GetStreamAsync(file.Address, ct)) { using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) { await response.CopyToAsync(fileStream, ct); } } if (File.Exists(file.SaveLocation)) { File.Delete(file.SaveLocation); } File.Move(tempPath, file.SaveLocation); } catch (Exception e) { Logger.Warn(e.ToString()); FailedDownloadCount++; } finally { if (File.Exists(tempPath)) { try { File.Delete(tempPath); } catch {} } await Task.Delay(250, ct); } } }
总结
优先尝试临时文件中转的方案,这是最通用且能彻底解决问题的办法;同时调低并行度可以减少触发问题的概率。如果想找到根因,用Process Explorer排查占用进程是最直接的方式。
内容的提问来源于stack exchange,提问作者user5405648

