【问题标题】:Issues with "void async" in Winform event handlers - and maybe a solution?Winform 事件处理程序中的“无效异步”问题 - 也许是一个解决方案?
【发布时间】:2022-09-23 21:05:18
【问题描述】:

据我所知,何时调用“async void”方法(例如事件处理程序),调用者永远无法知道它何时完成(因为它不能等待Task 完成) .所以有效地它是一个火灾和忘记电话。

用这段代码演示了这一点(我已经将一个 Button 和 TabControl 放在一个表单上并连接了 2 个事件)。单击按钮时,它会更改选项卡,这会导致引发 SelectedIndexChanged 事件,这是异步的。

    private void button1_Click(object sender, EventArgs e)
    {
        Debug.WriteLine(\"Started button1_Click\");
        tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
        Debug.WriteLine(\"Ended button1_Click\");
    }

    private async void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
    {
        Debug.WriteLine(\"Started tabControl1_SelectedIndexChanged\");
        await Task.Delay(1000);
        Debug.WriteLine(\"Ended tabControl1_SelectedIndexChanged\");
    }

结果输出是

Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended button1_Click
Ended tabControl1_SelectedIndexChanged

如您所见,SelectedIndexChanged 事件处理程序被触发,但调用者没有等待它完成(它不能等待,因为它没有要等待的任务)。

我提出的解决方案

事件处理程序不使用async,而是等待它使用的任何Async 方法,然后一切似乎都正常工作......它通过在调用DoEvents 时轮询Task.IsCompleted 属性来等待,以保持异步任务处于活动状态并且处理(在本例中为 Task.Delay)。

    private void button1_Click(object sender, EventArgs e)
    {
        Debug.WriteLine(\"Started button1_Click\");
        tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
        Debug.WriteLine(\"Ended button1_Click\");
    }

    private void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
    {
        Debug.WriteLine(\"Started tabControl1_SelectedIndexChanged\");
        Await(Task.Delay(1000));
        Debug.WriteLine(\"Ended tabControl1_SelectedIndexChanged\");
    }

    public static void Await(Task task)
    {
        while (task.IsCompleted == false)
        {
            System.Windows.Forms.Application.DoEvents();
        }

        if (task.IsFaulted && task.Exception != null)
            throw task.Exception;
        else
            return;
    }

这现在给出了预期的结果

Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended tabControl1_SelectedIndexChanged
Ended button1_Click

任何人都可以看到采用这种方法的任何问题吗???

  • Application.DoEvents(); 被我们的团队认为是一个错误,不允许任何人签入此类代码。它允许重入调用,这可能会导致许多难以诊断的错误。而且您的解决方案也会阻塞,并且在某些情况下可能会引入死锁。所以不,不要那样做。
  • 此外,通过强制代码等待,您已经阻止了 UI。关于async/await 的全部要点是您不要阻止用户界面!
  • DoEvents 使 UI 保持活跃和处理,代码并没有真正阻塞,因为它仍在处理异步任务列表,就像您使用异步等待一样
  • The DoEvents() is very likely to introduce bugs. 这里最大的问题是你为什么要这样做?你真正想解决什么问题?
  • 如果您在等待数据填充时确实必须阻止您的 UI,您应该查看this thread 以了解如何同步调用异步方法。

标签: c# winforms asynchronous async-await


【解决方案1】:

它通过在调用 DoEvents 时轮询 Task.IsCompleted 属性来等待

这是blocking on asynchronous code 的一种形式,我称之为嵌套消息循环黑客。

任何人都可以看到采用这种方法的任何问题吗?

是的。这种解决方案受到我所说的“意外重入”的影响。

从历史上看,意外的重入是许多人的原因,许多错误,因为某些代码最终不可避免地会从其他代码(在同一堆栈上)中调用。在您的示例中,tabControl1_SelectedIndexChanged(或更具体地说,Await)可以直接执行任何其他 UI 代码。包括对tabControl1_SelectedIndexChanged 的另一个调用。一旦你的代码变得不平凡,这就会导致问题,更糟糕的是,它依赖于时间,所以你会得到“heisenbugs”。

有一句老话:“DoEvents is evil”。这需要仔细考虑。

为什么不是所有与 DoEvents 相关的问题也与 await 相关?

这是个好问题;许多开发人员最初对await 持怀疑态度,因为他们被DoEvents 严重烧毁。原因await没有落入这个陷阱是因为只有一个消息循环。 await返回到那个单一的消息循环。没有嵌套的消息循环,因此没有意外的重新进入。

我永远不会推荐 DoEvents 作为解决方案。总是有更好的解决方案。

不保留处理顺序。例如,我以编程方式更改选项卡,并希望事件处理程序加载选项卡数据,然后我想对加载的数据做一些事情,目前我设置选项卡,事件触发 - 它开始加载选项卡数据并立即返回,事件处理程序调用返回,然后我尝试与部分加载的选项卡进行交互。此类问题发生在您无意中触发异步事件处理程序的任何地方

所以,这是实际的问题:C# 事件(至少是绝大多数事件的void-returning 事件)功能不足以充当通知以外的任何内容。在设计方面,C#事件允许你实现Observer pattern,但是是错误的选择实施Strategy pattern

要应用于您的示例,tabControl1_SelectedIndexChanged 只是一个通知(观察者模式),用于通知您的代码选项卡索引已更改。它不是提供数据加载(策略模式)的钩子,并且试图以这种方式使用它是导致这里实际问题的原因。

那么解决方案是不是依赖事件处理程序来驱动你的逻辑。看看那里的一些模型-视图-视图模型 (MVVM) 概念以获得一些灵感。在这种情况下,使用 ViewModel 类型的方法可能会有所帮助。这样您的代码根本不会更新选项卡控件;相反,它会更新 ViewModel,并且您的 VM 可以异步完成工作(根本没有 async void),然后您的代码可以在工作完成后更新 UI。

一种简单的方法(没有数据绑定和显式 VM)可能类似于:

private void button1_Click(object sender, EventArgs e)
{
  Debug.WriteLine("Started button1_Click");
  var newTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
  ShowLoadingState(newTab);
  tabControl1.SelectedTab = newTab;
  await LoadDataForTab(newTab); 
  Debug.WriteLine("Ended button1_Click");
}

private async Task LoadDataForTab(TabPage tab)
{
  await Task.Delay(1000);
  // load data into tab
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-28
    • 1970-01-01
    • 1970-01-01
    • 2022-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-06
    相关资源
    最近更新 更多