【问题标题】:How do I implement InvokeRequired UI pattern inside an async method?如何在异步方法中实现 InvokeRequired UI 模式?
【发布时间】:2015-01-19 08:09:25
【问题描述】:

假设我有一个尝试处理多线程环境的Form;因此,它会在完成任何 UI 修改之前检查它是否在 UI 线程上运行:

partial class SomeForm : Form
{
    public void DoSomethingToUserInterface()
    {
        if (InvokeRequired)
        {
            BeginInvoke(delegate { DoSomethingToUserInterface() });
        }
        else
        {
            … // do the actual work (e.g. manipulate the form or its elements)
        }
    }
}

现在假设我在该方法的 部分执行了一些冗长的操作;因此,我想使用 async/await 使其异步。

鉴于我应该更改方法签名以返回Task 而不是void(以便可以捕获异常),我将如何实现执行BeginInvoke 的部分?它应该返回什么?

public async Task DoSomethingToUserInterfaceAsync()
{
    if (InvokeRequired)
    {
        // what do I put here?
    }
    {
        … // like before (but can now use `await` expressions)
    }
}

【问题讨论】:

  • 顺便说一句,我正在考虑提取冗长的操作,一旦完成,将其结果作为参数传递给DoSomethingToUserInterface,它不再需要是async(从而避免了这个问题此处介绍)。不过,我仍然对解决方案感兴趣。
  • 一般来说,async/await 应该不需要Invoke()。 IE。您曾经遇到过从潜在的非 GUI 线程上下文调用这些方法的情况,现在您应该能够配置您的代码来完全避免这种情况。因此,与其问如何在这里更改方法签名,不如问如何更改调用者以避免首先需要调用。
  • @PeterDuniho:这似乎是一个合理的建议。所以我基本上必须追溯导致 DoSomethingToUserInterfaceAsync 的调用者链(直到某些可能是 Windows 窗体 UI 事件处理程序的方法),让所有这些调用者方法也使用 async/await
  • @stakx 为什么会问这个问题? await 保证异步操作后的代码会在原线程上运行。如果该线程是 UI 线程,则代码将在 UI 线程上运行,您根本不需要 BeginInvoke。您的 async 方法的调用者是否使用 Task.Run
  • @PanagiotisKanavos:我问是因为我现在才学习所有这些,还不知道对于像你这样更有经验的人来说什么是显而易见的。 - 你的评论很有帮助。我想我现在明白了what Peter Duino suggested above。谢谢!

标签: c# winforms async-await c#-5.0 invokerequired


【解决方案1】:

当使用async-await 和自定义awaiter(例如常见的TaskAwaiter)时,SynchronizationContext 会为您隐式捕获。稍后,当异步方法完成时,将使用 SynchronizationContext.Post 将延续(await 之后的任何代码)封送回相同的同步上下文。

这完全消除了使用InvokeRequired 和其他用于在 UI 线程上执行工作的技术的需要。为了做到这一点,您必须将您的方法调用一直跟踪到顶级方法调用,并将它们重构为可能使用async-await

但是,要按原样解决具体问题,您可以做的是在 Form 初始化时捕获 WinFormSynchronizationContext

partial class SomeForm : Form
{
    private TaskScheduler _uiTaskScheduler;
    public SomeForm()
    {
        _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    }
}

以后想用的时候用await:

if (InvokeRequired)
{
    Task uiTask = new Task(() => DoSomethingToUserInterface());
    uiTask.RunSynchronously(_uiTaskScheduler);
}
else
{
    // Do async work
}

【讨论】:

  • 首先,.NET 已经为您做到了。如果确实需要 Invoke,则其余代码中存在错误。其次,SynchronizationContext.Post 是一种在上下文上运行委托的更简单方法,根本不涉及任务
  • @PanagiotisKanavos 你不能await 打电话给SynchronizationContext.Post。你说后者更好?我不这么认为。
  • 关于第二部分,你是对的。 RunSynchronously 只是 SyncContext.Post 的包装。关于第一部分,如果需要Invoke,为什么代码有bug?
  • @SriramSakthivel 我不确定 OP 是否想要 await InvokeRequired 中的正文声明。
  • @PanagiotisKanavos 它浪费 CPU?如果您的程序对内存分配如此敏感,那么您将遇到不同的问题。 OP根本没有说明任何内容。它不会隐藏任何东西,因为您明确地将委托传递给正在执行的Task。它也没有隐藏 错误,它隐藏了对 async-await 模式的误解,我和 Sriram 都表示应该是实际使用的模式。
【解决方案2】:

您可以使用TaskScheduler.FromCurrentSynchronizationContext 获取当前同步上下文的任务调度程序(用于 UI 线程),并将其存储在一个字段中以供以后使用。

然后,当您有兴趣在 UI 线程中启动任何任务时,您必须将 uiScheduler 传递给 StartNew 方法,以便 TPL 将在提供的调度程序中调度任务(本例中为 UI 线程) .

不管怎样,你决定在 UI 线程中运行这些东西,所以只需将它安排到 UIScheduler,你不需要检查InvokeRequired

public async Task DoSomethingToUserInterfaceAsync()
{
    await Task.Factory.StartNew(() => DoSomethingToUserInterface(), CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    ...
}

要检索 UI 调度器,可以使用以下代码

private TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

注意: 非常重要的是,TaskScheduler.FromCurrentSynchronizationContext 只能从 UI 线程调用,否则会抛出异常,或者你会得到一个 TaskScheduler 用于其他一些 SynchronizationContext这不会满足您的需求。

另外请注意,如果您已从 UI 线程本身启动异步操作,则不需要上述任何魔法。 await 将在它开始的上下文中恢复。

【讨论】:

  • FromCurrentSynchronizationContext 只会在方法调用在 UI 线程上执行时为您提供 WinFormsSynchronizationContext。如果BeginInvoke 是必需的(因此为真),他不在 UI 线程上,很可能会得到默认的ThreadPoolSynchronizationContext
  • @YuvalItzchakov 阅读了我的第一行答案。 您必须存储 UI 任务调度程序
  • 对。这可能会产生误导,因为它并不能解释太多。也许您应该详细说明在 UI 线程上运行时需要存储同步上下文这一事实。
  • @Sriram:我该怎么做? (你能举个例子吗?)
  • @stakx 我更新了我的答案,如果有任何不清楚的地方或者您需要更多帮助,请给我留言。
猜你喜欢
  • 2011-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 2015-03-29
相关资源
最近更新 更多