【问题标题】:WinForms events executing out of order with async awaitWinForms 事件在异步等待中乱序执行
【发布时间】:2022-04-01 00:20:18
【问题描述】:

WinForms 事件出现故障。我想我明白为什么,但我很好奇解决它的最佳方法。

在下面的(人为的)代码中,首先调用Load,但是当到达await 然后调用Shown 时它会放弃控制。一旦Shown完成,Load终于可以完成了。

private async void SomeForm_Load(object sender, EventArgs e)
{
    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";
}

private void SomeForm_Shown(object sender, EventArgs e)
{
    someLabel.Text = "Shown";
}


private async Task SomeMethod()
{
    await Task.Delay(1).ConfigureAwait(false);
}

我需要确保Load 完成后才能执行Shown

这是我目前的解决方案:


Task _isLoadedTask;

private async void SomeForm_Load(object sender, EventArgs e)
{
    var tcs = new TaskCompletionSource();
    _isLoadedTask = tcs.Task;

    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";

    tcs.TrySetResult();
}

private async void SomeForm_Shown(object sender, EventArgs e)
{
    await _isLoadedTask;

    someLabel.Text = "Shown";
}

这样做的明显缺点是我必须手动设置边界/依赖项。确保事件按顺序执行的最佳方法是什么?

【问题讨论】:

  • 您是否尝试过重写 OnLoad 方法而不是订阅 Load 事件?
  • 我没有。我将如何使用OnLoad 解决它?
  • 我很确定你不能。唯一真正的选择是将该代码移出“SomeForm”的创建者,让他们在调用.Show...之前触发/等待异步部分(评论因为我有一段时间没有接触过WinForms,也许现在有一些异步支持)
  • 这个问题有用吗? Task sequencing and re-entracy
  • 这是一场你无法获胜的线程竞赛,ConfigureAwait() 调用使情况变得更糟。该代码看起来太假了,无法提出解决方法,但请考虑根本不使用事件。创建一个调用表单构造函数的异步方法,执行异步舞蹈,然后调用 Show() 结束。

标签: c# winforms async-await


【解决方案1】:

所有解决方案都需要阻止 Shown 继续直到加载完成。 TaskCompletionSource 似乎是实现这一目标的最优雅方式。

Task _isLoadedTask;

private async void SomeForm_Load(object sender, EventArgs e)
{
    var tcs = new TaskCompletionSource();
    _isLoadedTask = tcs.Task;

    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";

    tcs.TrySetResult();
}

private async void SomeForm_Shown(object sender, EventArgs e)
{
    await _isLoadedTask;

    someLabel.Text = "Shown";
}

【讨论】:

    【解决方案2】:

    在这种情况下,覆盖OnLoad 或订阅Load 具有相同的效果。

    但是,订阅 Load 会导致委托实例化和调用,而覆盖 OnLoad 不会也将给予对将执行的内容和执行顺序的更多控制。

    试试这个:

    private Task isLoadedTask;
    
    protected override void OnLoad(EventArgs e)
    {
        this.isLoadedTask = OnLoadAsync();
    
        base.OnLoad(e);
    }
    
    private async Task LoadAsync()
    {
        someLabel.Text = "Start Load";
        await SomeMethod();
        someLabel.Text = "Finish Load";
    }
    
    protected override async void OnShown(EventArgs e)
    {
        base.OnShown(e);
    
        await _isLoadedTask;
    
        someLabel.Text = "Shown";
    }
    

    【讨论】:

    • Paulo 在应用程序代码中覆盖受保护的OnLoadOnShown 等方法是惯用的、脆弱的和冗长的恕我直言。忘记调用base.On... 方法太容易了,接下来的一周要花几个小时调试您的应用程序,以找出未引发表单事件的原因。在编写库代码(创建自定义控件)而不是应用程序代码时,重写这些方法是有意义的。
    • 覆盖而不是订阅与实际问题完全不同。你可以对事件做同样的事情。
    【解决方案3】:

    正在以正确的顺序获取事件。事件处理程序中的async voidawait 混淆了这个问题。从Form 类的角度来看,当它调用你的Load 事件处理程序时,当SomeMethod 中的await 被命中时,事件处理程序方法返回。当SomeMethod 任务完成时,Load 事件处理程序中的其余代码作为 continuation 返回 UI 线程执行 - 尽管它看起来仍在 Load 事件处理程序中,因为是您的代码所在的位置,这不是编译器发出的。有点混乱,肯定的。 Jon Skeet 的书C# in Depth: Fourth Edition 可能是我所见过的对此最好的解释之一。

    此外,尽可能避免使用async void 方法,因为抛出异常可能会使您的进程崩溃。为了解决这两个问题并增加一些清晰度,我将使用 Load 事件来启动任务,然后在 Shown 中附加一个延续。这可确保当 Load 中的异步内容完成并显示时您想要运行的代码发生。

    private Task _loadTask = Task.CompletedTask;
    private Stopwatch _sw = Stopwatch.StartNew();
    
    private void Form1_Load(object sender, EventArgs e)
    {
        Trace.TraceInformation("[{0}] {1} Form1_Load - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
        _loadTask = DoAsyncLoadStuff();
        Trace.TraceInformation("[{0}] {1} Form1_Load - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
    }
    
    private void Form1_Shown(object sender, EventArgs e)
    {
        Trace.TraceInformation("[{0}] {1} Form1_Shown - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
        _loadTask.ContinueWith(DoStuffAfterShownAndAsyncLoadStuff, null, TaskScheduler.FromCurrentSynchronizationContext());
        Trace.TraceInformation("[{0}] {1} Form1_Shown - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
    }
    
    private void DoStuffAfterShownAndAsyncLoadStuff(Task asyncLoadTask, object context)
    {
        Trace.TraceInformation("[{0}] {1} DoStuffAfterShownAndAsyncLoadStuff. Task was {2}", Thread.CurrentThread.ManagedThreadId, 
            _sw.ElapsedMilliseconds, asyncLoadTask.Status);
    }
    
    private async Task DoAsyncLoadStuff()
    {
        await Task.Delay(5000).ConfigureAwait(false);
        Trace.TraceInformation("[{0}] {1} DoAsyncLoadStuff - Returning", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
        //throw new NotImplementedException();
    }
    

    上面的示例将在 Visual Studio 的“输出”窗口中输出线程 ID 和经过的毫秒数。调整延迟时间并取消注释抛出异常。请注意,DoStuffAfterShownAndAsyncLoadStuff 将始终在 UI 线程上执行。

    【讨论】:

    • Steven _loadTask.ContinueWith 方法返回一个Task,您的代码会触发并忘记它,这意味着该任务引发的任何异常都将被吞没。这不好。此外,通常应避免使用 async void,除非将其用于其预期目的,即使异步事件处理程序成为可能。在这种情况下,OP 将其用于其预期目的,因此没有理由警告他们。因此我投了反对票。
    猜你喜欢
    • 2020-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-27
    • 2013-08-01
    • 1970-01-01
    相关资源
    最近更新 更多