第二次运行任务时捕获OperationCanceledException失败原因咨询
异步下载二次取消后抛出未处理异常问题分析
问题描述
首次取消异步下载任务时,可正常捕获OperationCanceledException;但第二次调用DownloadStart()方法时,虽会先捕获该异常,却随后抛出未处理异常。
相关代码
private IProgress<long>? _progress = null; private double _timeOutMinutes = 5; private CancellationTokenSource? _cts = default; public FormActualizar(string versionApp) { _versionApp = versionApp; _progress = new Progress<long>(ProgressReport); InitializeComponent(); } private async void FormActualizar_Shown(object sender, EventArgs e) { try { await DownloadStart(); await DownloadCompleted(); } catch (OperationCanceledException) { await DownloadCanceled(); } catch (Exception ex) { await DownloadError(ex.Message); } } private async Task DownloadStart() { try { await SetForm(); if (_cts != null) _cts.Dispose(); _cts = new CancellationTokenSource(); using (var client = new HttpClient()) { client.Timeout = TimeSpan.FromMinutes(_timeOutMinutes); using (var file = new FileStream(_instaladorFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) { await client.DownloadAsync(_downloadUrl, file, _progress, _cts.Token); } } } catch (Exception) { throw; } } public static class HttpClientExtensions { public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<long>? progress = null, CancellationToken cancellationToken = default) { try { using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) { var contentLength = response.Content.Headers.ContentLength; using (var download = await response.Content.ReadAsStreamAsync()) { if (progress == null || !contentLength.HasValue) { await download.CopyToAsync(destination); return; } await download.CopyToAsync(destination, contentLength.Value, 81920, progress, cancellationToken); } } } catch (Exception) { throw; } } } public static class StreamExtensions { public static async Task CopyToAsync(this Stream source, Stream destination, long length, int bufferSize, IProgress<long>? progress = null, CancellationToken cancellationToken = default) { try { if (source == null) throw new ArgumentNullException(nameof(source)); if (!source.CanRead) throw new ArgumentException("Has to be readable", nameof(source)); if (destination == null) throw new ArgumentNullException(nameof(destination)); if (!destination.CanWrite) throw new ArgumentException("Has to be writable", nameof(destination)); if (bufferSize < 0) throw new ArgumentOutOfRangeException(nameof(bufferSize)); var buffer = new byte[bufferSize]; long totalBytesRead = 0; int bytesRead = 0; while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { cancellationToken.ThrowIfCancellationRequested(); await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; long porcentaje = ((100 * totalBytesRead / length)); progress?.Report(porcentaje); } } catch (Exception) { throw; } } } private async void btnStartStop_Click(object sender, EventArgs e) { if (btnStartStop.Text == "Cancelar") { _cts?.Cancel(); _cts?.Dispose(); } else { await DownloadStart(); } }
问题原因
CancellationTokenSource不当释放:在取消逻辑中,调用_cts?.Cancel()后立即执行_cts?.Dispose(),但此时异步下载任务可能仍在运行。已释放的令牌会失效,后续任务抛出的ObjectDisposedException无法被正确捕获。async void方法的异常逃逸:btnStartStop_Click是async void类型方法,这类方法的异常不会被调用者的try-catch捕获,会直接抛到同步上下文成为未处理异常。第二次调用DownloadStart是在该方法内,异常无法被FormActualizar_Shown的捕获逻辑处理。- 新旧任务的
_cts冲突:DownloadStart中直接释放旧_cts并创建新实例,但旧任务可能还在使用已释放的令牌,导致新任务启动后,旧任务的异常逃逸。
修复方案
1. 修正取消逻辑,延迟释放CancellationTokenSource
取消后不立即释放_cts,交给DownloadStart在创建新实例前安全处理:
private async void btnStartStop_Click(object sender, EventArgs e) { if (btnStartStop.Text == "Cancelar") { _cts?.Cancel(); } else { try { await DownloadStart(); } catch (OperationCanceledException) { await DownloadCanceled(); } catch (Exception ex) { await DownloadError(ex.Message); } } }
2. 安全管理CancellationTokenSource生命周期
在DownloadStart中处理旧_cts的取消与释放,确保旧任务完成后再清理:
private async Task DownloadStart() { try { await SetForm(); // 处理旧的CancellationTokenSource var oldCts = _cts; if (oldCts != null) { oldCts.Cancel(); try { // 等待旧任务完成(若有正在运行的任务) } finally { oldCts.Dispose(); } } _cts = new CancellationTokenSource(); using (var client = new HttpClient()) { client.Timeout = TimeSpan.FromMinutes(_timeOutMinutes); using (var file = new FileStream(_instaladorFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) { await client.DownloadAsync(_downloadUrl, file, _progress, _cts.Token); } } } catch (Exception) { throw; } }
3. 为async void方法添加完整异常捕获
由于WinForms点击事件只能是async void,必须在方法内部捕获所有异常避免逃逸:
private async void btnStartStop_Click(object sender, EventArgs e) { try { if (btnStartStop.Text == "Cancelar") { _cts?.Cancel(); } else { await DownloadStart(); await DownloadCompleted(); } } catch (OperationCanceledException) { await DownloadCanceled(); } catch (Exception ex) { await DownloadError(ex.Message); } }
内容的提问来源于stack exchange,提问作者EnMiPcFunciona
相关产品推荐
相关产品推荐

