清理带有InvokeRequired的代码垃圾

49

我知道当从任何非UI线程操纵UI控件时,必须将调用调度到UI线程以避免问题。一般的共识是应该使用测试InvokeRequired,并且如果为true,则使用.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测试,而不必在代码中到处复制粘贴这个模式?


1
你几乎不可能有一段代码被一个线程使用,同时也能在UI线程上执行。如果你可以使用lambda表达式,那么就不必测试InvokeRequired了。 - Hans Passant
@Hans - 我不知道,我在控制系统工作,我有一些能够实现这个功能的代码。我经常有多个线程同时运行异步处理操作,其中任何一个线程(除了UI线程)都可以调用共享日志记录、指示器更新或全局进程停止的方法。每个线程负责处理进程的不同方面,但它们也需要访问某些核心方法,通过Invoke进行调用。由于本地操作员也必须通过UI访问这些方法中的许多方法,所以它们最终共享了很多代码。 - J...
1
InvokeRequired仅适用于UI,不要在其他任何地方使用它。共享日志仅需要锁定。在软实时程序中,在帮助方法中隐藏上下文切换是一种相当危险的反模式。 - Hans Passant
2
记录到文件中,没错,但当同时记录到屏幕或添加项目到列表框时,它就成为了一个UI问题。许多这些操作都是整个过程的一部分。虽然让工作线程在需要时逐行调用可能会做到这一点,但大多数情况下,让UI线程处理它们更容易 - 让工作线程委托整个事情并继续进行更关键的操作。重点是,这不是不可能的情况。 - J...
那基本上就是我所处的情况。最终我没有使用我“接受”的帮助程序,而是继续使用了InvokeRequired测试。 - Erik Forbes
https://dev59.com/XHRB5IYBdhLWcg3wAjXR#10209287 是一个有效的观点。控件的句柄应该被创建以便 Invoke 起作用。为了安全起见,始终检查 InvokeRequired - nawfal
8个回答

68

好的,那这样如何:

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 });
}

4
除了我还添加了public static T InvokeIfRequired(this Control control, Func<T> function),这样当我想要返回一个值时就可以使用它。 - Davy8
这种模式也可以用于其他情况。在我的公司,我们使用Funs.CatchLog(Action a, string name)来捕获和记录“被遗忘”的异常。Funs.Measure(Action a, string name)测量了在一个操作中花费的时间等等... - user434817

9
我一直在阅读有关在非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线程上执行大部分工作而不是通过Invoke()多次调用UI线程是有优势的,并且可以极大地提高效率。

5
在任何平台上调用的成本大约为35微秒。我想对于某些人来说这是巨大的,而对于许多其他人来说则可以忽略不计。不错的实验。 - J...

9

在UI线程中调用Invoke效率略低。

相反,您可以创建一个名为InvokeIfNeeded的扩展方法,它需要一个Action参数。(这也允许您从调用点中删除new Action(...)


@Gabe:它与消息队列进行交互。 - SLaks
2
SLaks: Invoke 是同步的,所以如果它尝试在 UI 线程上与消息队列交互,那么它不会死锁吗? - Gabe
@Gabe:不是的。它检查是否在UI线程上,并且如果是,则同步地清空队列。(换句话说,这是一个可重入调用) - SLaks
2
SLaks:你的意思是Invoke会导致你的操作在任何其他排队的调用之后执行,而不是在其他排队的调用之前执行。改变顺序如何使其效率低下? - Gabe
@Gabe:不调用Invoke将不会运行任何排队的调用。此外,队列可能涉及锁定。 - SLaks
SLaks:但是那些排队的调用最终仍会在UI线程上运行,只是稍晚一些。 - Gabe

5
我意识到已经有一个几乎完美的答案one answer that's pretty much spot on,但我也想发表一下我的看法(我也在这里here发布了)。
我的方法略有不同,在处理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));

在看到这个答案之前,我想出了一个类似于你第二种方法的方法。在我的情况下,我需要这样做是为了调用ListViewItem集合属性并使用Linq读取和转换其项。 - kwill

2
我并不认为 Control.Invoke 是更新UI的最佳选择。我不能确定你的情况,因为我不知道何时调用UpdateSummary。然而,如果您定期调用它以显示进度信息(这是我从代码片段中得到的印象),那么通常有更好的选择。该选项是让UI线程轮询状态,而不是让工作线程推送它。
考虑在此情况下使用轮询方法的原因是:
  • 它打破了Control.Invoke强加的UI和工作线程之间的紧密耦合。
  • 它将更新UI线程的责任放在UI线程上,这本来就应该属于它。
  • UI线程可以决定何时以及多久更新。
  • 没有风险导致UI消息泵被超负荷,这将是由工作线程发起的调度技术的情况。
  • 工作线程无需等待确认已执行更新后再继续进行下一步操作(即在UI和工作线程上获得更高的吞吐量)。
因此,考虑创建一个System.Windows.Forms.Timer,定期检查要在Control上显示的文本,而不是从工作线程启动推送。同样,如果不了解您的确切要求,我不愿意说这绝对是您需要采取的方向,但在大多数许多情况下,它比Control.Invoke选项更好。
显然,这种方法完全消除了InvokedRequired检查的必要性。不用担心,它还简化了UI /工作线程交互的所有其他方面。

1
我认为这并不涵盖 UI / 工作线程交互的所有方面。事实上,现在你必须在工作线程的 GetCurrentStatus 函数周围添加同步。因此,根据工作线程已经有多复杂,你可能会引入各种线程问题。 - John Gietzen
@John:也许可以,但通常模式是将进度/状态信息封装在一个不可变类中,并通过写入共享变量作为单个交互点来发布它。这里唯一需要的就是volatile关键字。这似乎比在多个位置使用Control.Invoke并考虑它对两个线程的影响要简单得多。不过,我已经编辑了我的答案,以使用更少威慑性的语气来表达该观点。 - Brian Gideon

2

我对于仅用于查看的控件的首选方法是将所有控件状态封装在一个类中,可以在无需经过任何不一致状态的情况下进行更新(一种简单的方法是将所有需要更新的内容放在一个不可变的类中,并在需要更新时创建该类的新实例)。然后有一个方法将 Interlocked.Exchange 一个 updateNeeded 标志,如果没有挂起的更新但 IsHandleCreated 为真,则开始调用更新过程。更新过程应该在做任何更新之前首先清除 updateNeeded 标志(如果此时有人试图更新控件,则会发出另一个请求)。请注意,必须准备好捕获和忽略异常(我认为是 IllegalOperation),以防在准备更新控件时它被处理。

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


1

我目前无法发表评论,希望有人能看到这个并将其添加到被接受的答案中,否则它是完全正确的。

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

如此编写的被接受的答案将无法编译,因为ISynchronizeInvoke.Invoke()没有像Control.Invoke()一样只有1个参数的重载。

还有一件事是,使用可能更清晰,如下所示:
summary.InvokeIfRequired(c => { summary.Text = text; });而不是原文 summary.InvokeIfRequired(c => { textBox.Text = text });


0

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


我在这种情况下无法使用BackgroundWorker,因为我需要对线程本身进行相当精细的控制。不过还是谢谢你 =) - Erik Forbes
并不总是可能这样做。如果您试图做更多的事情而不仅仅是报告进度,ReportProgress 也可能会变得混乱。 - rollsch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接