是否猖獗使用Control.Invoke和Control.InvokeRequired健康?

时间:2014-03-12 09:29:21

标签: c# multithreading winforms thread-safety invoke

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

Form1加载并创建ServerHostServerHost已开始收听TcpClient个关联,在已连接和已接受的情况下ServerHost通过ThreadPool.QueueUserWorkItem(DoWork, client)生成一个帖子;

DoWork()主题中,我想更新Form1上的Winform Controls。

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

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

使用Control.Invoke和Control.InvokeRequired健康吗?我不擅长线程,MSDN说要使用BackgroundWorker,但我不知道如何在这里做。有任何改变结构的建议,以避免在此设置中使用Invoke吗?

5 个答案:

答案 0 :(得分:3)

Control.Invoke非常值得怀疑,Control.InvokeRequired非常有毒。

如果可能,请使用新的async / await支持,并且您不需要显式封送回UI线程。另外,使用Task.Run代替ThreadPool.QueueUserWorkItem进行后台工作。

Control.Invoke的问题在于它将您的库绑定到特定的UI(WinForms)。捕获SynchronizationContext比高于此值,并使用SynchronizationContext隐式捕获await甚至更好。

答案 1 :(得分:2)

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

一般来说,有几种方法可以做到这一点:

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

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

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

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

总结一下,除非您想要更改代码以使用Control.Invoke / async,否则应使用await

答案 2 :(得分:1)

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

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

如果您不需要结果值(或者至少不同步)并且您开始遇到性能问题,请查看BeginInvoke,它会异步处理调用。这意味着您的网络线程不必等待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附加文本,然后等待另一个连接

答案 3 :(得分:0)

我从来没有遇到过这样做的问题。无论您如何设置,都必须在创建它们的线程上更新控件。如果您使用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;
}

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

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

答案 4 :(得分:0)

一个非常明显的简化是将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");