使用async/await时如何更好地处理已处理的控件

Zer*_*er0 13 c# asynchronous winforms 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...
Run Code Online (Sandbox Code Playgroud)

请注意,每次await我也检查IsDisposed.这是必要的,因为说我await长时间跑步Task.同时,用户在完成之前关闭表单.在Task将完成并运行一个试图访问已释放的窗体上的控件延续.发生异常.

有没有更好的方法来处理这个或简化这种模式?我await在UI代码中使用自由,IsDisposed如果我忘记的话,每次都要检查它并且容易出错.

编辑:

有一些提议的解决方案不符合要求,因为它们会改变功能.

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

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

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

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

  • 使用a 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

澄清

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

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

编辑#2

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

bri*_*ght 4

在这种情况下,我还用来IsDisposed检查控件的状态。虽然它有点冗长,但并不比处理情况所需的冗长多——而且一点也不混乱。像带有 monad 的 F# 这样的函数式语言可能会有所帮助 - 我不是专家 - 但这看起来和 C# 中的一样好。

  • @fjch1997 控件处置不会发生在您要更新控件的主 UI 线程上吗?如果是这样,我预计当您在 UI 线程上运行任何代码时,它要么被释放,要么不被释放。 (2认同)