为何CancellationToken无法中途取消WebClient文件下载?
问题:CancellationToken无法中途取消WebClient下载的原因分析
我正在下载一个1GB的文件,尝试按'c'触发取消操作。如果在下载开始前快速按下'c',取消可以生效;但下载启动后,程序会下载完整文件,仅在下载结束后将Cancelled标记设为true。为什么CancellationToken无法中途取消下载?
代码示例
using System.Net; Console.WriteLine("Hello, World!"); Console.WriteLine("Downloading a 100MB file..."); var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; DownloadFileWithToken(cancellationToken); Console.WriteLine("Press C, to cancel the download"); if (Console.ReadKey().KeyChar == 'c') { cancellationTokenSource.Cancel(); Console.WriteLine("\n\rCancellation Sent"); } Console.ReadLine(); void DownloadFileWithToken(CancellationToken token) { const string file1Gb = "https://speed.hetzner.de/1GB.bin"; var webClient = new WebClient(); token.Register(() => webClient.CancelAsync()); webClient.DownloadProgressChanged += (sender, e) => { Console.WriteLine(e.BytesReceived); }; webClient.DownloadDataCompleted += (sender, e) => { if (!e.Cancelled) { byte[] data = (byte[])e.Result; Console.WriteLine($"\nDownload Completed: {data.Length}"); } else { Console.WriteLine("\nDownload canceled"); } }; webClient.DownloadDataAsync(new Uri(file1Gb)); }
场景示例
- 快速取消示例:

- 下载启动后尝试取消示例:

- 下载完成后才标记取消示例:

问题根源
- WebClient的设计局限性:
WebClient的异步方法基于旧的APM(异步编程模型)实现,它的CancelAsync()并非真正即时取消——只是设置内部标记,只有当下载操作到达下一个“检查点”时才会终止。但大文件下载时,WebClient会持续接收数据直到缓冲区填满或请求完成,不会主动中断TCP连接。 - 代码执行时序问题:代码中先启动下载,再进入等待用户输入的阻塞逻辑。当用户按下'c'时,下载已进行一段时间,
WebClient的CancelAsync()无法中断正在进行的数据接收流程。
解决方法
改用现代的HttpClient配合CancellationToken,它支持真正的即时取消,会直接中断底层HTTP请求:
using System.Net.Http; using System.Threading; Console.WriteLine("Hello, World!"); Console.WriteLine("Downloading a 1GB file..."); var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; // 启动下载任务 var downloadTask = DownloadFileWithToken(cancellationToken); Console.WriteLine("Press C to cancel the download"); if (Console.ReadKey().KeyChar == 'c') { cancellationTokenSource.Cancel(); Console.WriteLine("\n取消指令已发送"); } try { await downloadTask; } catch (OperationCanceledException) { Console.WriteLine("\n下载已取消"); } catch (Exception ex) { Console.WriteLine($"\n下载出错: {ex.Message}"); } Console.ReadLine(); async Task DownloadFileWithToken(CancellationToken token) { const string file1Gb = "https://speed.hetzner.de/1GB.bin"; using var httpClient = new HttpClient(); using var response = await httpClient.GetAsync(file1Gb, HttpCompletionOption.ResponseHeadersRead, token); response.EnsureSuccessStatusCode(); var totalBytes = response.Content.Headers.ContentLength ?? -1; var receivedBytes = 0; using var stream = await response.Content.ReadAsStreamAsync(token); using var memoryStream = new MemoryStream(); var buffer = new byte[8192]; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) { memoryStream.Write(buffer, 0, bytesRead); receivedBytes += bytesRead; Console.WriteLine($"已接收字节数: {receivedBytes}"); // 主动检查取消标记,确保即时响应 token.ThrowIfCancellationRequested(); } Console.WriteLine($"\n下载完成: {memoryStream.Length} 字节"); }
关键改进点
HttpClient的异步方法配合CancellationToken可直接中断HTTP请求,实现真正的中途取消。- 使用
HttpCompletionOption.ResponseHeadersRead,避免一次性加载整个文件到内存,同时允许更早响应取消指令。 - 在流读取循环中主动调用
token.ThrowIfCancellationRequested(),确保取消指令被即时处理。
内容的提问来源于stack exchange,提问作者José Alonso
相关产品推荐
相关产品推荐

