清理使用InvokeRequired乱丢的代码

时间:2010-10-06 15:26:24

标签: c# multithreading invoke invokerequired

我知道在从任何非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测试,而不必在整个地方复制和粘贴此模式?

8 个答案:

答案 0 :(得分:64)

那怎么样:

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

答案 1 :(得分:8)

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

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

答案 2 :(得分:7)

我一直在阅读关于添加逻辑检查的来回的参数,以确定在不在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线程上]: 300ms
  • TimedAction :: Go()+ 0 - 调试:[DEBUG]秒表[在UI线程上调用]: 299ms
  • TimedAction :: Go()+ 0 - 调试:[DEBUG]秒表[在UI线程上单独调用]: 649ms

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

答案 3 :(得分: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));

答案 4 :(得分:3)

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

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

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

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

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

答案 5 :(得分:2)

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

顺便提一下,如果一个控件尚未加入一个线程(通过添加到一个可见窗口,或者它的窗口变得可见),直接更新它是合法的,但使用BeginInvoke或Invoke是不合法的在它上面。

答案 6 :(得分: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 });

答案 7 :(得分:0)

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