winforms 如何从任务.运行回UI线程封送事件?

tkqqtvp1  于 2023-01-09  发布在  其他
关注(0)|答案(6)|浏览(160)

我有一个方法是“部分”异步的,意思是一个代码路径异步运行,另一个同步运行,我现在不能使同步部分异步,虽然我可能在将来可以。

public async Task UpdateSomethingAsync(){

    if (ConditionIsMet){
    
      await DoSomethingAsync;
   
    }else{

      DoSomethingSynchronous;
    }
}

DoSomethingAsync和DoSomethingSynchronous都是I/O绑定的。使用“await”从Winforms UI线程调用此方法会导致它在采用Synchronous路径时阻塞UI线程,这是预期的。

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This blocks if the DoSomethingSynchronous path is taken, causing UI to 
   //become unresponsive.
    await UpdateSomethingAsync();  

}

所以我去了Stephen Cleary的博客。他的建议(虽然是CPU绑定代码而不是I/O绑定)是用Task.Run运行该方法,就好像它是完全同步的一样,同时记录该方法是“部分”异步的。但是,DoSomethingSynchronous引发的事件现在会导致异常,我相信这是因为它们现在处于与UI不同的线程上。

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This no longer blocks, but events will not marshal back to UI Thread 
   //causing an exception.
    await Task.Run(()=> UpdateSomethingAsync());  
}

如何解决此问题?

zd287kbt

zd287kbt1#

不要更新UI、绑定到UpdateSomethingAsync内部UI的任何模型或它调用的任何方法。创建一个class,它将保存更新UI所需的数据,并从UpdateSomethingAsync返回该类的示例。
DoSomethingAsync将返回一个Task<ThatClassYouCreated>,而DoSomethingSynchronous只返回ThatClassYouCreated的一个示例。然后,在awaitUpdateSomethingAsync之后,返回MyDropDownBox_DropDownClosed,使用UpdateSomethingAsync返回的示例更新UI或模型。

public class UpdatedInformation
{
    public int UpdateId { get; set; }
    public string UpdatedName { get; set; }
    public DateTimeOffset Stamp { get; set; }
    // etc, etc...
}

public class YourForm : Form
{
    private async Task<UpdatedInformation> DoSomethingAsync()
    {
        var result = new UpdatedInformation();
        // Something is awaited...
        // Populate the properties of result.
        // Do not modify your UI controls. Do not modify the model bound to those controls.
        return result;
    }
    
    private UpdatedInformation DoSomethingSynchronous()
    {
        var result UpdatedInformation();
        // Populate the properties of result.
        // Do not modify your UI controls. Do not modify the model bound to those controls.
        return result;
    }

    private async Task<UpdatedInformation> UpdateSomethingAsync()
    {
        if (ConditionIsMet)
        {
            return await DoSomethingAsync();
        }
        else
        {
            return await Task.Run(DoSomethingSynchronous);
        }
    }

    private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
    {
        var updatedInformation = await UpdateSomethingAsync();
        // Now use updatedInformation to update your UI controls, or the model bound to
        // your UI controls.
        model.Id = updatedInformation.UpdateId;
        // etc...
    }
}
jdg4fx2g

jdg4fx2g2#

在事件处理程序中,可以使用Invoke()更新UI,如下所示:

private void someEventHandler() // <- it might have params
{

    // ... possibly some other code that does NOT update the UI ...

    this.Invoke((MethodInvoker)delegate {

        // ... it's safe to update the UI from in here ...

    });

    // ... possibly some other code that does NOT update the UI ...

}

我不知道是谁一直在做这件事,但我在这篇文章下面的评论一直被删除。
这回答了问题的标题,即:
如何从任务.运行回UI线程封送事件?
当您从不同的线程接收到事件时,这是一种非常有效的更新UI的方法。

ssgvzors

ssgvzors3#

如果您声明“[..] DoSomethingSynchronous [is] I/O bound”,您也可以通过将DoSomethingSynchronous中的IO绑定操作 Package 在Task.Run中来使其异步。
如果DoSomethingSynchronous是这样的

public void DoSomethingSynchronous(...) 
{
    // some UI work

    // blocking sysnchornous IO operation
    var res = IoOperation();

    // some more UI work
}

你可以改写成。

public async Task DoSomethingSynchronous(...) 
{
    // some UI work

    // no-UI-Thread blocking IO operation
    var res = await Task.Run(() => IoOperation()).ConfigureAwait(true);

    // some more UI work
}

.ConfigureAwait(true)可以省略,但是确保在等待之后的代码将在原始同步上下文(即UI线程)中被调度。
然后,您显然需要重命名方法等,但是如果有一天您可以在DoSomethingSynchronous中使用真正的asycn IO,那么这将使代码更易于维护

6gpjuf90

6gpjuf904#

由于UpdateSomethingAsync需要访问UI上下文,因此不应将其 Package 在Task.Run调用中(您很少需要从Task.Run调用async方法,通常只有在方法实现不正确且无法修复时才需要调用)。
相反,DoSomethingSynchronous应该是您从Task.Run调用的东西。毕竟,该方法的目的是在线程池线程中异步运行同步方法。因此,仅将其用于您希望在线程池线程中运行的同步方法,而不是(假定的)需要访问UI上下文的异步方法。

wf82jlnq

wf82jlnq5#

WinUI 3遵循以下方法。

DispatcherQueue.TryEnqueue(() =>
{
     //Code to Update the UI
});
lg40wkob

lg40wkob6#

我想我会在做更多的研究后自己回答这个问题。大多数其他的答案在某种程度上是正确的,但不一定能一下子解释整个问题,所以我会在这里总结一下。
这个问题的第一个片段是按事件方式工作的,但是如果使用UpdateSomethingAsync中的Synchronous路径,则会阻塞。事件工作是因为“await”自动捕获UI线程的SynchronizationContext(这是关键),这样,从UpdateSomethingAsync引发的任何事件都会通过SynchronizationContext整理回UI。这只是使用async/await的正常方式:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //This blocks if the DoSomethingSynchronous path is taken, causing UI to 
     //become unresponsive, but events propagate back to the UI correctly.
     await UpdateSomethingAsync();  

  }

Task.Run的工作方式大致相同,* 如果您不使用它来运行异步方法的话。* 换句话说,它不会阻塞,并且仍然会向UI线程发送事件,因为UpdateSomethingAsync被替换为Synchronous方法。这只是Task.Run的正常用法:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //UpdateSomethingAsync is replaced with a Synchronous version, and run with
     // Task.Run.

     await Task.Run(UpdateSomethingSynchronously());  

  }

然而,问题中的原始代码 * 是 * Async,因此上面的内容不适用。问题提出了以下代码片段作为可能的解决方案,但当事件引发时,它会错误地输出对UI的非法跨线程调用,* 因为我们正在使用Task.Run来调用Async方法,并且由于某种原因,这不会设置SynchronizationContext:*

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //This no longer blocks, but events raised from UpdateSomethingAsync
     //will cause an Illegal Cross Thread Exception to the UI, because the
     //SyncrhonizationContext is not correct.  Without the SynchronizationContext,
     //events are not marshalled back to the UI thread.
     await Task.Run(()=> UpdateSomethingAsync());  
  }

看起来起作用的是使用Task.Factory.StartNew将UI同步上下文分配给使用TaskScheduler.FromCurrentSynchronizationContext的任务,如下所示:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This doesn't block and will return events to the UI thread sucessfully,
    //because we are explicitly telling it the correct SynchronizationContext to use.
    await Task.Factory.StartNew(()=> UpdateSomethingAsync(),
                              System.Threading.CancellationToken.None,
                              TaskCreationOptions.None,
                              TaskScheduler.FromCurrentSynchronizationContext);  
}

同样有效的方法是简单地将DoSomethingSynchronous Package 在Task.Run中,这种方法非常简单,但对调用者来说有点“谎言”:

public async Task UpdateSomethingAsync(){

    if (ConditionIsMet){

      await DoSomethingAsync;

    }else{

      await Task.Run(DoSomethingSynchronous);
    }
}

我认为这是一个小谎言,因为该方法并不是真正的完全异步的,因为它派生了一个线程池线程,但可能永远不会给调用者带来问题。
希望这是有道理的。如果其中任何一个被证明是不正确的请让我知道,但这是我的测试所揭示的。

相关问题