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

标签 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将完成并运行尝试访问已处理表单上的控件的延续。发生异常。

有没有更好的方法来处理这个或简化这个模式?我用 await在 UI 代码中大量使用,而且检查 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 我正在引入一个错误)。理想情况下,我想编写可以封装此基本设计的包装器、扩展方法等。但我想不出办法做到这一点。

    另外,我想我必须声明性能是一流的考虑因素。例如,由于我不会讨论的原因,抛出异常非常昂贵。所以我也不想只尝试 catch ObjectDisposedException每当我做 await .甚至更丑陋的代码也会损害性能。好像只是在做一个 IsDisposed每次检查都是最好的解决方案,但我希望有更好的方法。

    编辑 #2

    关于性能 - 是的,这都是相对的。我知道绝大多数开发人员并不关心抛出异常的成本。抛出异常的真正成本是题外话。在其他地方有很多关于这方面的信息。可以说它比 if (IsDisposed) 贵许多数量级就够了。查看。对我来说,不必要地抛出异常的代价是无法接受的。在这种情况下我说不需要,因为我已经有了一个不会抛出异常的解决方案。再次,让延续抛出 ObjectDisposedException不是一个可接受的解决方案,正是我试图避免的。

    最佳答案

    我也用 IsDisposed在这种情况下检查控件的状态。虽然它有点冗长,但它并不比处理情况所必需的更冗长——而且它一点也不令人困惑。像 F# 这样带有 monad 的函数式语言可能会有所帮助——我不是专家——但这似乎和 C# 中的一样好。

    关于c# - 使用 async/await 时如何更好地处理已处置的控件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31062282/

    相关文章:

    c# - 防止在 RichTextBox 中自动滚动

    c# - 按 T​​AB 键时绕过 DataGridView 中的只读单元格

    .net - 访问绑定(bind)到 DataGridView 行的项目 (WinForms)

    c# - 如何在 C# 中只编译一个文件?

    c# - Entity Framework - 同时执行多个查询时出错

    c# - 将 C# RSACryptoServiceProvider 转换为 JAVA 代码

    javascript - 使用 Nodejs 的多个用户输入

    PowerShell,使用 Start-Job 和 Start-Process 测试异步任务的性能/效率

    javascript - 异步函数没有按应有的顺序执行

    c# - 向 C# 控制台应用程序添加 "--help"参数