【问题标题】:Cleaning up code littered with InvokeRequired [duplicate]清理乱七八糟的代码 InvokeRequired [重复]
【发布时间】:2010-10-06 15:26:23
【问题描述】:

我知道,当从任何非 UI 线程操作 UI 控件时,您必须将调用编组到 UI 线程以避免出现问题。普遍的共识是您应该使用 test 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 测试,而不必到处复制和粘贴此模式?

【问题讨论】:

  • 很少有线程使用的代码也可以在 UI 线程上执行。是的,当您可以使用 lambda 时,不要费心测试 InvokeRequired。
  • @Hans - 我不知道,我在控制系统工作,我有代码可以做到这一点。我经常有许多线程运行异步进程操作,除了 UI 线程之外,其中任何一个都可以调用共享日志记录、指示器更新或调用以暂停全局进程。每个线程处理进程的不同方面,但它们还必须访问某些核心方法,这些方法通过 Invoke 调用。由于本地操作员还必须通过 UI 访问其中许多方法,因此他们最终会共享大量代码。
  • InvokeRequired 仅对 UI 是必需的,不要用于其他任何事情。共享日志只需要一个锁。在软实时程序中,将上下文切换隐藏在辅助方法中是一种相当危险的反模式。
  • 记录到文件,是的,但是当同时记录到屏幕或将项目添加到列表框时,它就会成为 UI 问题。许多这些动作也是整个程序的一部分。虽然在大多数情况下让工作线程通过它们并根据需要逐行调用可能会更容易让 UI 线程处理它们 - 让工作人员委托整个事情并继续他们的更多工作时间紧迫的操作。重点是,这不是不可能的情况。
  • 这几乎就是我所在的船。我最终没有使用我“接受”的助手,而是保留了 InvokeRequired 测试。

标签: c# multithreading invoke invokerequired


【解决方案1】:

这个怎么样:

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

【讨论】:

  • @SLaks:嗯,你比我快 3 秒,但我得到了密码。 :)
  • 我做了完全相同的事情,除了我还添加了public static T InvokeIfRequired(this Control control, Func&lt;T&gt; function) 以及当我想要返回一个值时。
  • 此模式也可用于其他情况。在我的公司,我们使用Funs.CatchLog(Action a, string name) 来捕获和记录“被遗忘”的异常。 Funs.Measure(Action a, string name) 衡量一个动作花费了多少时间等等......
  • 我建议使用 ISynchronizeInvoke 而不是 Control。
  • @JohnGietzen 尝试您的 sn-p 时出现以下错误:“方法 'Invoke' 没有重载需要 1 个参数。”当我使用 Control 而不是 ISynchronizeInvoke 尝试它时,它工作得很好!
【解决方案2】:

从 UI 线程调用 Invoke 效率有点低。

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

【讨论】:

  • @Gabe:它与消息队列交互。
  • SLaks: Invoke 是同步的,所以如果它试图与UI线程上的消息队列交互会不会死锁?
  • @Gabe:不。它检查它是否在 UI 线程上,如果是,则同步清空队列。 (换句话说,这是一个可重入调用)
  • SLaks:您是说Invoke 导致您的操作在任何其他排队调用之后执行,而不是在其他排队调用之前执行。更改顺序如何使其效率低下?
  • @Gabe:不调用 Invoke 将不会运行任何排队的调用。此外,队列可能涉及锁。
【解决方案3】:

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

结果可能会让你们中的一些人感到惊讶(这些测试是通过 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())是有利的,并且大大提高了效率。

【讨论】:

  • ...所以在您使用的任何平台上调用大约需要 35 微秒。我想对于一些人来说是巨大的,而对于其他许多人来说是微不足道的。不错的实验。
【解决方案4】:

我意识到已经有 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 来读取和转换它的项目。
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

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

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

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

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

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

    【讨论】:

    • 我不认为这“简化了 UI/工作线程交互的所有方面”。事实上,现在你必须在 worker 上围绕 GetCurrentStatus 函数添加同步。因此,根据 worker 的复杂程度,您可能会引入大量线程问题。
    • @John:也许吧,尽管一般模式是将进度/状态信息打包在一个不可变类中,并通过将其写入共享变量作为单点交互来发布。这里唯一需要的是volatile 关键字。这似乎比在多个地方使用Control.Invokeing 并且必须考虑它对 both 线程造成的影响要简单得多。不过,我确实编辑了我的答案,以便在这一点上使用一种不那么强烈的语气。
    【解决方案7】:

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

    control.Invoke(new Action(() =&gt; action(control))); 应改为
    control.Invoke(new Action(() =&gt; action(control)), null);

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

    另一件事是,用法可能更清楚
    summary.InvokeIfRequired(c =&gt; { summary.Text = text; });,而不是书面 summary.InvokeIfRequired(c =&gt; { textBox.Text = text });

    【讨论】:

      【解决方案8】:

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

      【讨论】:

      • 在这种情况下我不能使用 BackgroundWorker,因为我需要对线程本身进行非常细粒度的控制。不过谢谢 =)
      • 并非总是可以这样做。如果您尝试做的不仅仅是逐字报告进度,那么 ReportProgress 也会变得一团糟。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-16
      • 1970-01-01
      相关资源
      最近更新 更多