使用async / await

时间:2015-06-25 23:08:23

标签: c# winforms asynchronous async-await

考虑在UI线程上运行的代码:

dividends = await Database.GetDividends();
if (IsDisposed)
    return;
//Do expensive UI work here
earnings = await Database.GetEarnings();
if (IsDisposed)
    return;
//Do expensive UI work here
//etc...

请注意,每次await我都会检查IsDisposed。这是必要的,因为在长期运行的await上说我Task。同时,用户在完成之前关闭表单。 Task将完成并运行继续尝试访问已处置表单上的控件。发生异常。

有没有更好的方法来处理或简化这种模式?我在UI代码中大量使用await,每次检查IsDisposed都很难看,如果我忘了就会出错。

修改

有一些提议的解决方案不适合该法案,因为它们会改变功能。

  • 防止表单关闭直到后台任务完成

这会让用户感到沮丧。而且它仍然允许发生可能昂贵的GUI工作,这是浪费时间,伤害性能并且不再相关。在我几乎总是从事背景工作的情况下,这可以防止表格在很长一段时间内关闭。

  • 隐藏表单并在完成所有任务后关闭它

这有防止表单关闭的所有问题,除非不会让用户感到沮丧。执行昂贵GUI工作的延续仍将继续。它还增加了所有任务完成后跟踪的复杂性,然后在隐藏时关闭表单。

  • 使用CancellationTokenSource取消表单关闭时的所有任务

这甚至无法解决问题。事实上,我已经这样做了(浪费背景资源也没有意义)。这不是一个解决方案,因为由于隐式竞争条件,我仍然需要检查IsDisposed。以下代码演示了竞争条件。

public partial class NotMainForm : Form
{
    private readonly CancellationTokenSource tokenSource = new CancellationTokenSource();

    public NotMainForm()
    {
        InitializeComponent();
        FormClosing += (sender, args) => tokenSource.Cancel();
        Load += NotMainForm_Load;
        Shown += (sender, args) => Close();
    }

    async void NotMainForm_Load(object sender, EventArgs e)
    {
        await DoStuff();
    }

    private async Task DoStuff()
    {
        try
        {
            await Task.Run(() => SimulateBackgroundWork(tokenSource.Token), tokenSource.Token);
        }
        catch (TaskCanceledException)
        {
            return;
        }
        catch (OperationCanceledException)
        {
            return;
        }
        if (IsDisposed)
            throw new InvalidOperationException();
    }

    private void SimulateBackgroundWork(CancellationToken token)
    {
        Thread.Sleep(1);
        token.ThrowIfCancellationRequested();
    }
}

当任务已经完成,表单已关闭且继续仍然运行时,会发生竞争条件。您会偶尔看到InvalidOperationException被抛出。当然,取消任务是一种很好的做法,但这并不能减轻我的检查IsDisposed

澄清

原始代码示例完全我想要的功能。它只是一个丑陋的模式,正在等待后台工作然后更新GUI"是一个非常常见的用例。从技术上讲,我只是希望如果表格被处理,延续就不会运行。示例代码就是这样做但不优雅并且容易出错(如果我忘记在每个IsDisposed上检查await我引入了一个错误)。理想情况下,我想编写一个封装器,扩展方法等,可以封装这个基本设计。但我无法想办法做到这一点。

另外,我想我必须说明性能是一流的考虑因素。例如,抛出异常是非常昂贵的,因为我不会进入。因此,每当我执行ObjectDisposedException时,我也不想尝试捕捉await。甚至更丑陋的代码也会伤害性能。似乎只是每次进行IsDisposed检查是最好的解决方案,但我希望有更好的方法。

编辑#2

关于表现 - 是的,它都是相对的。据我所知,绝大多数开发人员都不关心抛出异常的成本。抛出异常的真实成本是偏离主题的。在其他地方有很多可用的信息。我只想说它比if (IsDisposed)支票要贵很多个数量级。对我来说,不必要地抛出异常的代价是不可接受的。在这种情况下我说不用,因为我已经有了一个不会抛出异常的解决方案。再一次,让延续抛出ObjectDisposedException不是一个可接受的解决方案而完全我试图避免的。

3 个答案:

答案 0 :(得分:2)

表单拥有CancellationTokenSource应该非常简单,并在表单关闭时调用表单Cancel

然后,您的async方法可以观察CancellationToken

答案 1 :(得分:2)

在这种情况下,我还使用IsDisposed来检查控件的状态。虽然它有点冗长,但处理这种情况并不是必要的冗长 - 而且根本不会让人感到困惑。像F#这样的函数式语言可能有助于monad - 我不是专家 - 但这看起来和C#一样好。

答案 2 :(得分:0)

我曾经通过不关闭表单解决了类似的问题。相反,我一开始就把它藏起来,只有当所有杰出的工作都完成后才真正关闭它。当然,我必须以Task变量的形式跟踪这项工作。

我发现这是一个干净的解决方案,因为根本不会出现处理问题。然而,用户可以立即关闭表单。