DownloadFileAsync 不会卡主线程,但因无取消支持、无进度回调、异常静默处理,易被误认为卡住;应改用 HttpClient + GetStreamAsync + CopyToAsync 并传入 CancellationToken。

DownloadFileAsync 会卡主线程吗?
不会卡,但容易误以为卡了——因为 DownloadFileAsync 本身不阻塞 UI 线程,但它默认不支持取消、没有进度回调、也不抛出网络异常到调用线程,一旦出错(比如 404、DNS 失败、连接超时),它只会静默失败或触发 DownloadFileCompleted 事件并把 e.Error 设为异常对象,而很多人没检查这个字段,就以为“没反应”“卡住了”。
常见错误现象:DownloadFileCompleted 事件压根没触发;下载完文件为空;UI 看似响应,但重试多次后才发现根本没发请求。
- 必须订阅
DownloadFileCompleted和DownloadProgressChanged两个事件,缺一不可 -
DownloadFileAsync是基于WebClient的,而WebClient在 .NET Core 5+ 已标记为过时,新项目优先用HttpClient配合GetStreamAsync - 如果目标 URL 重定向(302),
WebClient默认跟随,但某些认证场景下会丢 Header,需手动处理
怎么安全地取消正在下载的文件?
DownloadFileAsync 本身不提供取消机制,调用 CancelAsync() 只能终止尚未发出的请求,对已建立连接的下载无效。真要支持取消,得换方案。
使用场景:用户点了“取消下载”,或窗口关闭前需中止后台任务。
- 改用
HttpClient+GetStreamAsync+CopyToAsync,传入CancellationToken - 别直接用
DownloadFileAsync绑定 UI 控件的 Click 事件——它没有返回Task,无法 await,也接不到取消信号 - 若坚持用
WebClient,只能靠设置Timeout(通过反射改私有字段)或用Abort()强制断开,但后者不稳定,可能抛WebException且状态难清理
为什么下载大文件时内存暴涨甚至 OOM?
DownloadFileAsync 内部会把整个响应体读进内存再写入磁盘,对几百 MB 以上的文件,很容易触发 GC 压力或直接 OutOfMemoryException,尤其在 32 位进程或内存受限环境。
性能影响:不是慢,是危险——它不流式写盘,全量缓存。
- 绝对不要用
DownloadFileAsync下载 >50MB 的文件 - 正确做法:用
HttpClient.GetStreamAsync拿到响应流,再用Stream.CopyToAsync分块写入文件流,控制 buffer 大小(如 8192 字节) - 注意
FileStream要用FileOptions.Asynchronous(Windows)或至少设useAsync: true,否则CopyToAsync会退化成同步写
HttpClient 方式怎么写才不出错?
这是目前最稳的异步下载姿势,但细节多:超时设置、流释放、异常分类、重试逻辑都得自己兜底。
示例关键片段:
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 8192, useAsync: true);
await stream.CopyToAsync(file, 8192, cancellationToken);
-
EnsureSuccessStatusCode()必须调,否则 4xx/5xx 不抛异常,后续ReadAsStreamAsync可能返回空流 -
HttpClient实例应复用,不要每个下载都 new 一个,否则端口耗尽 -
cancellationToken要传给每一个 await 点,漏一个就取消失效 - 别在
using外提前 closeFileStream,.NET 6+ 的CopyToAsync写完不会自动 flush,需显式file.Flush()或确保Dispose触发
真正麻烦的从来不是“怎么下”,而是“下一半断网了怎么办”“磁盘满了写失败怎么清理临时文件”“服务端限速时如何平滑降级”。这些不在 API 层暴露,得自己补逻辑。









