【问题标题】:Best practice for async/await methods and exception handlingasync/await 方法和异常处理的最佳实践
【发布时间】:2016-01-07 19:22:08
【问题描述】:

我必须处理一些使用异步函数无法正常运行的旧东西,并且由于我对这个概念的理解有限,我正在努力寻找处理以下问题的最佳方法。

我有一个按钮,单击该按钮将执行长时间运行的工作(解压缩大型 ZIP 存档,需要几分钟)。它的Commandexecute 方法是这样定义的:

private async void Import()
{
    // some stuff
    tokenSource = new CancellationTokenSource();
    IProgress<ProgressReport> progress = new Progress<ProgressReport>(data => Report(data));

    try
    {
        await Task.Run(() =>
        {
            Backup(tokenSource.Token, progress);
            Unzip(tokenSource.Token, progress);
        });
    }
    catch(Exception)
    {
        // do some rollback operation
    }

以及像这样定义的等待函数:

private void Backup(CancellationToken token, IProgress<ProgressReport> progress)
{
token.ThrowIfCancellationRequested();

    var parent = Path.GetFullPath(Path.Combine(Paths.DataDirectory, ".."));
    if (!Directory.Exists(parent))
    {
        progress.Report(new ProgressReport(Level.Info, string.Format(
                "Root Directory ({0}) does not exist; Creating it.", parent)));
        Directory.CreateDirectory(parent);
        return;
    }

    if (!Directory.Exists(Paths.DataDirectory))
    {
        progress.Report(new ProgressReport(Level.Info, string.Format(
                "Data Directory ({0}) does not exist; nothing to backup.", Paths.DataDirectory)));
        return;
    }

    // Generate a name for the backup           
    try
    {
        progress.Report(new ProgressReport(Level.Info, string.Format(
                "Renaming source Data Directory ({0}) to a temporary name.", Paths.DataDirectory)));

        var temp = Path.Combine(parent, Guid.NewGuid().ToString("N"));
        Directory.Move(Paths.DataDirectory, temp);
        // Success, let's store the backupFolder in a class field
        backupFolder = temp;

        progress.Report(new ProgressReport(Level.Info, string.Format(
                "Source Data Directory ({0}) successfully renamed to {1}.", Paths.DataDirectory, backupFolder)));

        token.ThrowIfCancellationRequested();
    }
    catch (OperationCanceledException)
    {
        progress.Report(new ProgressReport(Level.Warn, "Cancelling Import Operation."));
        throw;
    }
    catch (Exception ex)
    {
        // some stuff then throw Exception to bubble-up
        throw;
    }
}

private async Task Unzip(CancellationToken token, IProgress<ProgressReport> progress)
{
    try
    {
        token.ThrowIfCancellationRequested();

        var parent = Path.GetFullPath(Path.Combine(Paths.DataDirectory, ".."));

        try
        {
            progress.Report(new ProgressReport(
                Level.Info, string.Format("Uncompressing export file {0}.", InputFileName)));

            using (var zipArchive = ZipFile.Open(InputFileName, ZipArchiveMode.Read, Encoding.UTF8))
            {
                var extractedCount = 0;
                var totalCount = zipArchive.Entries.Count;
                foreach (var entry in zipArchive.Entries)
                {
                    progress.Report(new ProgressReport(
                        Level.Off, string.Format("Extracting {0}, {1}/{2}",
                        entry.FullName, extractedCount + 1, totalCount), extractedCount, totalCount));

                    if (string.IsNullOrEmpty(entry.Name) && string.IsNullOrEmpty(entry.FullName))
                        continue;

                    if (string.IsNullOrEmpty(entry.Name))
                    {
                        var dir = Path.Combine(parent, entry.FullName);
                        if (!Directory.Exists(dir))
                            Directory.CreateDirectory(dir);
                    }
                    else entry.ExtractToFile(Path.Combine(parent, entry.FullName), true);

                    notPaused.WaitOne();
                    token.ThrowIfCancellationRequested();

                    extractedCount++;
                }

                // Everything completed, update the progress bar.
                progress.Report(new ProgressReport(totalCount, totalCount));
            }
        }
        catch (OperationCanceledException)
        {
            progress.Report(new ProgressReport(Level.Warn, "Cancelling Import Operation."));
            throw;
        }
        catch (Exception ex)
        {
            // Some stuff then throw Exception to bubble-up
            throw;
        }
    }
}

此时,异步作业运行良好,UI 没有冻结,但问题是 BackupUnzip 中抛出的 Exception 方法永远不会冒泡,也不会在 Import 中捕获方法,因此程序在throw 指令处崩溃。

经过一番研究,我在this msdn article 中发现这是使用void 返回方法时的正常行为。所以我稍微改变了程序,现在使用await 的调用是这样的:

try
{
    await Backup(tokenSource.Token, progress);
    await Unzip(tokenSource.Token, progress);
}

我的方法是这样定义的:

private async Task Backup(CancellationToken token, IProgress<ProgressReport> progress)
{
    // Same logic
    Task.Delay(1000);
}

private async Task Unzip(CancellationToken token, IProgress<ProgressReport> progress)
{
    // Same logic
    Task.Delay(1000);
}

现在,Import 方法会出现异常,但 UI 在整个作业完成时间内都被冻结,就像作业由 UI 线程处理一样。有什么问题的提示吗?

【问题讨论】:

  • 你还在用Task.Run吗?
  • 看起来你标记了你的方法async,但是让它们同步工作,而不是异步工作,但由于你没有展示它们的实现,所以没有办法说具体是什么,你做错了。
  • 我用方法实现编辑了我的帖子。不,我不再使用Task.Run
  • @BastienM。如果你想让这些方法在后台运行,你需要Task.Run
  • @BastienM。这是无关的。您可以使用Task.Run。在它内部调用一个异步方法并等待返回的任务。在这种情况下,异常会“冒泡”。

标签: c# wpf asynchronous exception-handling async-await


【解决方案1】:

感谢@i3arnon cmets,我设法实现了我想要的。 这是我的电话改变的方式:

await Task.Run(async () =>
{
    await Backup(tokenSource.Token, progress);
    await Unzip(tokenSource.Token, progress);
});

这两个函数仍然声明为private async Task

这样,由于Task.Run,作业在后台运行,因此 UI 不会冻结并且异常会正确冒泡。

【讨论】:

    猜你喜欢
    • 2017-01-31
    • 2018-01-03
    • 1970-01-01
    • 1970-01-01
    • 2017-05-11
    • 2013-05-09
    • 2011-11-10
    • 1970-01-01
    • 2013-04-22
    相关资源
    最近更新 更多