【问题标题】:Canceling WebClient download while waiting for download to complete在等待下载完成时取消 WebClient 下载
【发布时间】:2023-03-10 12:25:01
【问题描述】:

寻找更普遍接受的等待WebClient 的模式:

  • 下载文件(可能需要几百毫秒或几分钟)
  • 等待下载完成,然后再执行任何其他工作
  • 定期检查另一个类的标志 (bool) 并在需要时取消下载(不能修改此类)

约束:

  • 不能使用 async/await,除非它类似于 Task.Run(async () => await method())
  • Download方法被调用时,它只需要表现得像一个返回字符串的普通方法
  • 可以使用 .Net 4.5 和 Roslyn 编译器的任何功能
  • 使用WebClient.DownloadFileTaskAsyncDownloadFileAsync 没有区别;只需要能够根据需要使用WebClient 取消下载

当前的实现似乎正在运行,但似乎不太正确。在使用WebClient 时,是否有比使用while 循环和Thread.Sleep 定期检查otherObject.ShouldCancel 更普遍接受的替代方法?

private string Download(string url)
{
    // setup work
    string fileName = GenerateFileName();

    // download file
    using (var wc = new WebClient()) 
    {
        wc.DownloadFileCompleted += OnDownloadCompleted

        Task task = wc.DownloadFileTaskAsync(url, fileName);

        // Need to wait until either the download is completed
        // or download is canceled before doing any other work
        while (wc.IsBusy || task.Status == TaskStatus.WaitingForActivation) 
        {
            if (otherObject.ShouldCancel) 
            {
                wc.CancelAsync();
                break;
            }

            Thread.Sleep(100);
        }

        void OnDownloadCompleted(object obj, AsyncCompletedEventArgs args)
        {
            if(args.Cancelled)
            {
                // misc work
                return;
            }

            // misc work (different than other work below)
        }
    }

    // Other work after downloading, regardless of cancellation.
    // Could include in OnDownloadCompleted as long as this
    // method blocked until all work was complete

    return fileName;
}

【问题讨论】:

  • 您是否尝试过传入 CancellationToken 参数,然后使用 cancelToken.Register(webClient.CancelAsync);?
  • 我会说 while-sleep 循环可以满足您的要求。由于Download 应该是同步的 - 在等待下载完成时它无关紧要,所以为什么不同时轮询该标志。当然,您可以在所有这些之上添加一些花哨的东西,但这不会有太大变化。
  • @KhaledElKholy - otherObject.ShouldCancel 是否仍需要定期轮询以向 cancellationToken.Cancel() 发出信号?
  • 不,cancellationTokenSource.Cancel() 将导致 webClient.Cancel() 被调用,您已将其注册为回调,然后导致异步任务抛出 WebException 或 TaskCanceledException。另外,为什么不将 cancelTokenSource 实例传递给 otherObject 以便它可以调用 cancelTokenSource.Cancel() 而不必定期检查 otherObject.ShouldCancel 属性?你可以访问 otherObject 的类型内部实现吗?
  • @KhaledElKholy - 我无法修改 otherObject 中的任何内容(请参阅问题,第三个要点)。

标签: c# webclient cancellation webclient-download


【解决方案1】:

我希望这会有所帮助。 基本上,您的包装器使用 cancelToken.Register(webClient.Cancel); 注册一个回调;一旦 cancelToken.Cancel() 被调用,异步任务应该抛出一个异常,您可以按如下方式处理:

public class Client
{
    public async Task<string> DownloadFileAsync(string url, string outputFileName, CancellationToken cancellationToken)
    {
        using (var webClient = new WebClient())
        {
            cancellationToken.Register(webClient.CancelAsync);
            
            try
            {
                var task = webClient.DownloadFileTaskAsync(url, outputFileName);

                await task; // This line throws an exception when cancellationTokenSource.Cancel() is called.
            }
            catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
            {
                throw new OperationCanceledException();
            }
            catch (AggregateException ex) when (ex.InnerException is WebException exWeb && exWeb.Status == WebExceptionStatus.RequestCanceled)
            {
              throw new OperationCanceledException();
            }
            catch (TaskCanceledException)
            {
                throw new OperationCanceledException();
            }

            return outputFileName;
        }
    }
}

尝试这个例子的简单方法

    private async static void DownloadFile()
    {
        var cancellationTokenSource = new CancellationTokenSource();
        var client = new Client();

        var task = client.DownloadFileAsync("url",
            "output.exe", cancellationTokenSource.Token);

        cancellationTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));

        cancellationTokenSource.Cancel();

        try
        {
            var result = await task;
        }
        catch (OperationCanceledException)
        {
            // Operation Canceled
        }
    }

在更现实的场景中,cancellationTokenSource.Cancel() 将被由于用户交互或回调而引发的事件调用。

更新

另一种方法是订阅 DownloadProgressChanged 事件并在调用回调时检查 otherObject.ShouldCancel。

这是一个例子:

public class Client
{
    public string Download(string url)
    {
        // setup work
        string fileName = GenerateFileName();

        // download file
        using (var wc = new WebClient())
        {
            wc.DownloadProgressChanged += OnDownloadProgressChanged;
            wc.DownloadFileCompleted += OnDownloadFileCompleted;

            DownloadResult downloadResult = DownloadResult.CompletedSuccessfuly;

            void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
            {
                if (otherObject.ShouldCancel)
                {
                    ((WebClient)sender).CancelAsync();
                }
            }

            void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
            {
                if (e.Cancelled)
                {
                    downloadResult = DownloadResult.Cancelled;
                    return;
                }

                if (e.Error != null)
                {
                    downloadResult = DownloadResult.ErrorOccurred;
                    return;
                }
            }

            try
            {
                Task task = wc.DownloadFileTaskAsync(url, fileName);
                task.Wait();
            }
            catch (AggregateException ex)
            {
            }

            switch (downloadResult)
            {
                case DownloadResult.CompletedSuccessfuly:

                    break;
                case DownloadResult.Cancelled:

                    break;
                case DownloadResult.ErrorOccurred:

                    break;
            }
        }

        // Other work after downloading, regardless of cancellation.
        // Could include in OnDownloadCompleted as long as this
        // method blocked until all work was complete

        return fileName;
    }
}

public enum DownloadResult
{
    CompletedSuccessfuly,
    Cancelled,
    ErrorOccurred
}

【讨论】:

  • 欣赏答案。不过,我不完全确定这如何提供替代方法? 1) otherObject.ShouldCancel 未用于验证取消。 2) 现在有任意 5 秒超时 3) async/await 的使用被明确排除在一个选项中(见问题)。
猜你喜欢
  • 1970-01-01
  • 2023-01-28
  • 1970-01-01
  • 1970-01-01
  • 2022-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-24
相关资源
最近更新 更多