You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

第二次运行任务时捕获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();
    }
}

问题原因

  1. CancellationTokenSource不当释放:在取消逻辑中,调用_cts?.Cancel()后立即执行_cts?.Dispose(),但此时异步下载任务可能仍在运行。已释放的令牌会失效,后续任务抛出的ObjectDisposedException无法被正确捕获。
  2. async void方法的异常逃逸:btnStartStop_Click是async void类型方法,这类方法的异常不会被调用者的try-catch捕获,会直接抛到同步上下文成为未处理异常。第二次调用DownloadStart是在该方法内,异常无法被FormActualizar_Shown的捕获逻辑处理。
  3. 新旧任务的_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 22:46:04