【问题标题】:Is rampant use of Control.Invoke and Control.InvokeRequired healthy?大量使用 Control.Invoke 和 Control.InvokeRequired 是否健康?
【发布时间】:2014-03-12 09:29:21
【问题描述】:

我正在编写一个像这样工作的客户端服务器应用程序:

Form1 加载并创建ServerHostServerHost 开始监听TcpClient 连接,在连接和接受时,ServerHost 通过ThreadPool.QueueUserWorkItem(DoWork, client) 产生一个线程;

DoWork() 线程中,我想更新Form1 上的Winform 控件。

这是通过在ServerHost 中设置事件来实现的,例如ServerHost.SomethingHappened。当DoWork() 中发生某些事情时,它会引发事件并调用 Form1.Handler 来更新 winforms 控件。

这个设置给了我cross-thread operation error

Control.Invoke 和 Control.InvokeRequired 的使用是否健康?我不擅长线程,MSDN 说要使用BackgroundWorker,但我在这里看不到该怎么做。有什么建议可以更改结构以避免在此设置中使用 Invoke?

【问题讨论】:

  • 随着使用的猖獗,尤其是在许多工作线程的情况下,BeginInvoke() 的危害性远低于 Invoke()。

标签: c# multithreading winforms thread-safety invoke


【解决方案1】:

Control.Invoke 是非常值得怀疑的,Control.InvokeRequired 是完全有毒的。

如果可能,请使用新的 async/await 支持,您无需显式编组回 UI 线程。此外,使用Task.Run 而不是ThreadPool.QueueUserWorkItem 进行后台工作。

Control.Invoke 的问题在于它将您的库绑定到特定的 UI(WinForms)。捕获SynchronizationContext 是一个步骤,使用await 隐式捕获SynchronizationContext 更好。

【讨论】:

  • 唉...我还在 VS2010
  • @Jake:在这种情况下,我推荐SynchronizationContext
【解决方案2】:

您必须在 UI 线程上调用更新用户界面的代码。

一般来说,有几个选项可以做到这一点:

  • Control 上调用Invoke
  • 使用已在 UI 线程上启动的 BackgroundWorker
  • 在 UI 线程的 SynchronizationContext 上调用 Post
  • Task.ContinueWith 与UI 线程的TaskScheduler 一起使用
  • 使用带有async/await的异步调用

在我看来,最后一种方法对开发人员来说是迄今为止最简单的方法,但它仅适用于 C# 5 和 .NET 4.5 或带有 Microsoft.Bcl.Async package 的 .NET 4.0。任务几乎同样易于使用,但这两种方法都需要您更改代码。它们不能简单地从线程池线程调用 UI 线程上的方法。

BackgroundWorker 通常用于安排需要相当长的时间的操作。它的ReportProgress 方法在调用RunWorkerAsync 方法的线程上引发ProgressChanged 事件。因此,它也不是解决您问题的好方法。

SynchronizationContext.PostControl.Invoke 的工作方式类似,但 Control.Invoke 不需要您捕获 UI 上下文,因此更易于使用。

总而言之,您应该使用Control.Invoke,除非您想更改代码以使用async/await

【讨论】:

  • async/await 也通过Microsoft.Bcl.Async支持.NET 4.0。
  • @StephenCleary 谢谢!我不知道。但是我仍然需要 C# 5 编译器来支持 async/await 关键字?
  • 是的。那是 Visual Studio 2012 或更高版本。
【解决方案3】:

只要 UI 线程没有被这些调用负担过重就可以了。它确实给通信带来了一些延迟,这通常不是问题,但是,如果您正在做很多 Invokes,或者如果 UI 线程正在做很多工作,它可能会成为更大的问题(例如,渲染复杂的图形或类似的东西)。 Invoke 是一个同步方法——在调用的命令被实际处理之前它不会返回,并返回它的返回值。

只要您不受这些问题的困扰,一切都很好。分析和性能测试对于正确分配资源至关重要,猜测通常会浪费大量时间和资源。

如果您不需要结果值(或至少不需要同步)并且您开始遇到性能问题,请查看BeginInvoke,它以异步方式处理调用。这意味着您的网络线程不必等待 UI 线程工作。这在具有数千个连接的高性能服务器中非常关键。他们根本无法等待 UI 完成它的工作。

但是,请注意,在不同的线程上运行服务器套接字并不是大型服务器的好解决方案,事实上,它也不再是最简单的解决方案。 .NET 现在对异步调用和回调有很好的支持,使异步处理的实现变得轻而易举。在典型的 Winforms 应用程序中,这意味着 I/O 阻塞应用程序可以在没有不断运行和轮询线程的情况下工作。例如,等待新连接可以很简单:

var connection = await listener.AcceptTcpClientAsync();

就是这样。自动地,所有回调将在正确的时间被处理,而不会阻塞处理,您自己的所有代码始终在主 UI 线程上运行。换句话说,您可以轻松做到这一点:

while (!aborted)
{
  var connection = await listener.AcceptTcpClientAsync();

  tbxLog.Text += "New connection!\r\n";
}

虽然这看起来像是一个无限循环无限期地阻塞 UI 线程,但实际情况是,当应用程序到达 await 关键字时,它会注册一个异步回调并返回。只有在实际调用异步回调时(在这种情况下由 IOCP),代码才会恢复(在 UI 线程上),并且 tbxLog 附加了文本,然后等待 另一个 连接。

【讨论】:

    【解决方案4】:

    我从来没有遇到过这样的问题。无论您如何设置,更新您的控件都必须在创建它们的线程上完成。如果您使用 BackgroundWorker 或其他异步构造,则会在某个地方调用调用。我通常在表单上创建一个方法,例如:

    delegate void TextSetter(string text);
    internal void SetText(string text)
    {
      //call on main thread if necessary
      if (InvokeRequired)
      {
        this.Invoke((TextSetter)SetText, text);
        return;
      }
    
      //set the text on your label or whatever
      this.StatusLabel.Text = text;
    }
    

    我在许多应用程序中都使用过这种方法,它从来都不是问题,甚至每秒更新很多次。

    据我所知,绕过调用的唯一方法是让您的主线程不断轮询更新,这通常被认为是一种非常糟糕的做事方式。

    【讨论】:

      【解决方案5】:

      一个非常明显的简化是将 InvokeRequired/Invoke 抽象为 Control 的扩展方法。

      public static class FormExt {
          public static void Execute(this Control c, Action a) {
              if (c.InvokeRequired) {
                  c.Invoke(a);
              } else {
                  a();
              }
          }
      }
      

      现在您只需将普通表单更新包装到 lambda 中并执行它们。

      form1.Execute(() => form1.Text = "Hello world");
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-02-29
        • 2011-06-18
        • 2020-01-26
        • 2019-08-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多