据我所知,当一个“async void”方法(比如一个事件处理程序)被调用时,调用者永远不知道它什么时候完成(因为它不能等待Task
完成)。
下面的代码演示了这一点(我已经将一个Button和TabControl放到了一个窗体上,并连接了这两个事件)。当单击按钮时,它会更改选项卡,这会引发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
事件处理程序被激发,但调用者没有等待它完成(它不能等待,因为它没有要等待的Task)。
我建议的解决方案
事件处理程序不使用async
,而是等待它使用的任何Async
方法,然后一切似乎都工作了...它通过轮询Task.IsCompleted
属性来等待,同时调用DoEvents
,以保持异步任务的活动和处理(在本例中为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
有人能看到采取这种方法有什么问题吗?
1条答案
按热度按时间2w3kk1z51#
它在调用DoEvents时通过轮询Task.IsCompleted属性来等待
这是异步代码上的一种阻塞形式,我称之为嵌套消息循环攻击。
有人认为采取这种方法有什么问题吗?
是的。这种解决方案会遭受我所说的“意外重入”。
从历史上看,意外的重入是导致许多错误的原因,因为某些代码最终不可避免地会被其他代码调用(在相同的堆栈上)。在您的范例中,
tabControl1_SelectedIndexChanged
(或者更具体地说,Await
)可以直接执行任何其他UI代码。包括对tabControl1_SelectedIndexChanged
的另一次调用。一旦您的代码变得重要,这就会导致问题,甚至更糟,它与时间有关所以你会得到“海森堡”。有一句流传已久的话:“doEvents是邪恶的”。这值得仔细考虑。
为什么与DoEvent关联的所有问题都不与await关联呢?
问得好许多开发人员最初对
await
持怀疑态度,因为他们已经被DoEvents
严重烧伤。await
* 没有 * 落入这个陷阱的原因是,只有一个消息循环。await
* 返回 * 到那个单一的消息循环。没有嵌套的消息循环,因此没有意外的重入。我绝对不会推荐
DoEvents
作为解决方案,总有更好的解决方案。处理的顺序不被保留。例如,我以编程方式更改了选项卡,并期望事件处理程序加载选项卡数据,然后我想对加载的数据执行一些操作,当前我设置了选项卡,事件触发-它开始加载选项卡数据并立即返回,事件处理程序调用返回,然后我尝试与部分加载的选项卡交互。在您无意中触发异步事件处理程序的任何地方都会发生这种问题
所以,实际的问题是:C#事件(至少是
void
-返回事件,这是绝大多数事件)的功能不足以充当通知以外的任何其他功能。在设计方面,C#事件允许您实现Observer pattern,但 * 是实现Strategy pattern的错误选择 *。为了应用到您的示例中,
tabControl1_SelectedIndexChanged
只是一个通知(Observer模式),用于通知您的代码制表符索引发生了变化。它不是一个提供数据加载的钩子(Strategy模式),试图以这种方式使用它才是导致此处实际问题的原因。解决方案是 * 不 * 依赖于事件处理程序来驱动您的逻辑。看看一些Model-View-ViewModel(MVVM)概念,从中获得一些启发。使用ViewModel类的方法可能会在这种情况下有所帮助。这样,您的代码就根本不会更新选项卡控件;相反,它将更新ViewModel,并且您的VM可以异步地完成这项工作(根本没有
async void
),然后您的代码可以在工作完成时更新UI。一种简单的方法(没有数据绑定和显式VM)如下所示: