48

我知道从任何非 UI 线程操作 UI 控件时,您必须将调用编组到 UI 线程以避免出现问题。普遍的共识是您应该使用测试 InvokeRequired,如果为真,则使用 .Invoke 来执行封送处理。

这导致很多代码看起来像这样:

private void UpdateSummary(string text)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action(() => UpdateSummary(text)));
    }
    else
    {
        summary.Text = text;
    }
}

我的问题是:我可以省略 InvokeRequired 测试而只调用 Invoke,如下所示:

private void UpdateSummary(string text)
{
    this.Invoke(new Action(() => summary.Text = text));
}

这样做有问题吗?如果是这样,是否有更好的方法来保留 InvokeRequired 测试,而不必在整个地方复制和粘贴此模式?

4

8 回答 8

66

那么这个怎么样:

public static class ControlHelpers
{
    public static void InvokeIfRequired<T>(this T control, Action<T> action) where T : ISynchronizeInvoke
    {
        if (control.InvokeRequired)
        {
            control.Invoke(new Action(() => action(control)), null);
        }
        else
        {
            action(control);
        }
    }
}

像这样使用它:

private void UpdateSummary(string text)
{
    summary.InvokeIfRequired(s => { s.Text = text });
}
于 2010-10-06T15:29:52.283 回答
8

Invoke从 UI 线程调用有些低效。

相反,您可以创建一个带有参数的InvokeIfNeeded扩展方法。Action(这也将允许您new Action(...)从呼叫站点中删除)

于 2010-10-06T15:29:45.787 回答
8

我一直在阅读有关添加逻辑检查的参数,以确定在不在 UI 线程上而不是在 UI 线程本身上时是否应该使用调用 IFF。我编写了一个类来检查各种方法的执行时间(通过Stopwatch ),以粗略估计一种方法相对于另一种方法的效率。

结果可能会让你们中的一些人感到惊讶(这些测试是通过Form.Shown事件运行的):

     // notice that we are updating the form's title bar 10,000 times
     // directly on the UI thread
     TimedAction.Go
     (
        "Direct on UI Thread",
        () =>
        {
           for (int i = 0; i < 10000; i++)
           {
              this.Text = "1234567890";
           }
        }
     );

     // notice that we are invoking the update of the title bar
     // (UI thread -> [invoke] -> UI thread)
     TimedAction.Go
     (
        "Invoke on UI Thread",
        () =>
        {
           this.Invoke
           (
              new Action
              (
                 () =>
                 {
                    for (int i = 0; i < 10000; i++)
                    {
                       this.Text = "1234567890";
                    }
                 }
              )
           );
        }
     );

     // the following is invoking each UPDATE on the UI thread from the UI thread
     // (10,000 invokes)
     TimedAction.Go
     (
        "Separate Invoke on UI Thread",
        () =>
        {
           for (int i = 0; i < 10000; i++)
           {
              this.Invoke
              (
                 new Action
                 (
                    () =>
                    {
                       this.Text = "1234567890";
                    }
                 )
              );
           }
        }
     );

结果如下:

  • TimedAction::Go()+0 - 调试:[DEBUG] 秒表 [直接在 UI 线程上]:300 毫秒
  • TimedAction::Go()+0 - 调试:[DEBUG] 秒表 [在 UI 线程上调用]:299 毫秒
  • TimedAction::Go()+0 - 调试:[DEBUG] 秒表 [UI 线程上的单独调用]:649 毫秒

我的结论是,无论您是在 UI 线程还是工作线程上,您都可以随时安全地调用,而无需通过消息泵循环返回的大量开销。但是,在 UI 线程上执行大部分工作而不是对 UI 线程进行多次调用(通过Invoke())是有利的,并且大大提高了效率。

于 2010-10-06T18:32:54.753 回答
5

我意识到已经有一个答案非常合适,但我也想发表我对它的看法(我也在此处发布)。

我的有点不同,它可以稍微更安全地处理空控件,并且可以在必要时返回结果。当我尝试调用在可能为 null 的父窗体上显示 MessageBox 并返回显示该 MessageBox 的 DialogResult 时,这两种方法都派上了用场。


using System;
using System.Windows.Forms;

/// <summary>
/// Extension methods acting on Control objects.
/// </summary>
internal static class ControlExtensionMethods
{
    /// <summary>
    /// Invokes the given action on the given control's UI thread, if invocation is needed.
    /// </summary>
    /// <param name="control">Control on whose UI thread to possibly invoke.</param>
    /// <param name="action">Action to be invoked on the given control.</param>
    public static void MaybeInvoke(this Control control, Action action)
    {
        if (control != null && control.InvokeRequired)
        {
            control.Invoke(action);
        }
        else
        {
            action();
        }
    }

    /// <summary>
    /// Maybe Invoke a Func that returns a value.
    /// </summary>
    /// <typeparam name="T">Return type of func.</typeparam>
    /// <param name="control">Control on which to maybe invoke.</param>
    /// <param name="func">Function returning a value, to invoke.</param>
    /// <returns>The result of the call to func.</returns>
    public static T MaybeInvoke<T>(this Control control, Func<T> func)
    {
        if (control != null && control.InvokeRequired)
        {
            return (T)(control.Invoke(func));
        }
        else
        {
            return func();
        }
    }
}

用法:

myForm.MaybeInvoke(() => this.Text = "Hello world");

// Sometimes the control might be null, but that's okay.
var dialogResult = this.Parent.MaybeInvoke(() => MessageBox.Show(this, "Yes or no?", "Choice", MessageBoxButtons.YesNo));
于 2010-10-06T16:12:41.547 回答
2

我不相信这Control.Invoke是更新 UI 的最佳选择。在你的情况下,我不能肯定地说,因为我不知道在什么情况下UpdateSummary打电话。但是,如果您定期调用它作为显示进度信息的机制(这是我从代码片段中得到的印象),那么通常会有更好的选择。该选项是让 UI 线程轮询状态,而不是让工作线程推送它。

在这种情况下应该考虑轮询方法的原因是:

  • 它打破了 UI 和工作线程之间的紧密耦合Control.Invoke
  • 它将更新 UI 线程的责任放在它应该属于的 UI 线程上。
  • UI 线程可以决定更新的时间和频率。
  • 不存在 UI 消息泵溢出的风险,就像工作线程启动的编组技术那样。
  • 工作线程不必等待确认更新已执行,然后再继续执行后续步骤(即,您可以在 UI 和工作线程上获得更多吞吐量)。

因此,请考虑创建一个System.Windows.Forms.Timer定期检查要在 上显示的文本,Control而不是从工作线程启动推送。同样,在不知道您的确切要求的情况下,我不愿意明确地说这是您需要前进的方向,但在大多数情况下,它选择要好Control.Invoke

显然,这种方法完全消除了InvokedRequired检查的必要性。没关系,它简化了 UI/工作线程交互的所有其他方面。

于 2010-10-06T15:54:02.607 回答
2

对于仅查看控件,我首选的方法是将所有控件状态封装在一个类中,该类可以在不经历任何不一致状态的情况下进行更新(一种简单的方法是将所有需要更新的东西放在一起一个不可变的类,并在需要更新时创建该类的新实例)。然后有一个方法,它将 Interlocked.Exchange 一个 updateNeeded 标志,如果没有更新挂起但 IsHandleCreated 为真,则 BeginInvoke 更新过程。更新过程应该清除 updateNeeded 标志作为它做的第一件事,在做任何更新之前(如果有人试图在那个时候更新控件,另一个请求将被 BeginInvoked)。

顺便说一句,如果控件尚未加入线程(通过添加到可见窗口或使其所在的窗口变为可见),则直接更新它是合法的,但在其上使用 BeginInvoke 或 Invoke 是不合法的。

于 2010-10-06T16:10:19.803 回答
1

我还不能发表评论,希望有人会看到这一点并将其添加到已接受的答案中,否则就会出现。

control.Invoke(new Action(() => action(control)));应该读
control.Invoke(new Action(() => action(control)), null);

正如所写的那样,接受的答案不会编译,因为ISynchronizeInvoke.Invoke()没有像只有 1 个参数那样的重载Control.Invoke()

另一件事是用法可能
summary.InvokeIfRequired(c => { summary.Text = text; });比写 的更清楚summary.InvokeIfRequired(c => { textBox.Text = text });

于 2015-08-26T15:45:02.500 回答
0

如果可能的话,使用 BackgroudWorker 来使 UI 响应并使用 ReportProgress 更新 UI 会更容易,因为它与 UI 在同一线程上运行,因此您不需要 InvokeRequired。

于 2010-10-06T18:23:21.933 回答