任何时候初学者都会问:How to update the GUI from another thread in C#?,答案很简单:
if (foo.InvokeRequired)
{
foo.BeginInvoke(...)
} else {
...
}
但使用它真的很好吗?在非GUI线程执行foo.InvokeRequired
之后,foo
的状态可能会发生变化。例如,如果我们在foo.InvokeRequired
之后但在foo.BeginInvoke
之前关闭表单,则调用foo.BeginInvoke
将导致InvalidOperationException
:无法在控件上调用Invoke或BeginInvoke直到创建了窗口句柄。如果我们在调用InvokeRequired
之前关闭表单,就不会发生这种情况,因为即使从非GUI线程调用它也会false
。
另一个例子:假设 foo
是TextBox
。如果你关闭表单,那么非GUI线程执行foo.InvokeRequired
(这是假的,因为表单已关闭)和foo.AppendText
它将导致ObjectDisposedException
。
相反,在我看来,使用WindowsFormsSynchronizationContext
要容易得多 - 只有在线程仍然存在且使用Post
的同步调用抛出Send
时才会使用InvalidAsynchronousStateException
发布回调。如果线程不再存在。
是不是更容易使用WindowsFormsSynchronizationContext
?我错过了什么吗?如果它不是真的线程安全,为什么我应该使用InvokeRequired-BeginInvoke模式?你觉得哪个更好?
答案 0 :(得分:7)
WindowsFormsSynchronizationContext
通过将自身附加到绑定到创建上下文的线程的特殊控件来工作。
所以
if (foo.InvokeRequired)
{
foo.BeginInvoke(...)
} else {
...
}
可以用更安全的版本替换:
context.Post(delegate
{
if (foo.IsDisposed) return;
...
});
假设context
是在WindowsFormsSynchronizationContext
所在的同一UI线程上创建的foo
。
此版本避免了您提出的问题:
在非GUI线程执行foo.InvokeRequired之后,foo的状态可以改变。例如,如果我们在foo.InvokeRequired之后关闭表单,但在foo.BeginInvoke之前,调用foo.BeginInvoke将导致InvalidOperationException:在创建窗口句柄之前,无法在控件上调用Invoke或BeginInvoke。如果我们在调用InvokeRequired之前关闭表单,就不会发生这种情况,因为即使从非GUI线程调用它也会是假的。
如果您使用多个消息循环或多个UI线程,请注意WindowsFormsSynchronizationContext.Post
的一些特殊情况:
WindowsFormsSynchronizationContext.Post
才会执行该委托。如果没有没有任何反应并且没有引发异常。Application.Run
)委托将执行(这是因为系统维护每个线程的消息队列而不知道有人从中抽取消息的事实)WindowsFormsSynchronizationContext.Send
将抛出InvalidAsynchronousStateException
。但是如果它绑定的线程是活动的并且没有运行消息循环它将不会立即执行但仍将被放置在消息队列中并在执行Application.Run
时执行试。如果在自动处理的控件(如主窗体)上调用IsDisposed
,则这些情况都不会意外执行代码,因为委托将立即退出,即使它是在意外时间执行的。
危险的情况是调用WindowsFormsSynchronizationContext.Send
并考虑代码将被执行:它可能没有,现在有办法知道它是否做了什么。
我的结论是WindowsFormsSynchronizationContext
是一个更好的解决方案,只要它被正确使用。
它可以在复杂的情况下创建sublte问题但是常见的GUI应用程序只有一个消息循环,只要应用程序本身一直很好就会存在。
答案 1 :(得分:0)
谁说首选InvokeRequired
/ Control.BeginInvoke
?如果你问我,在大多数情况下,由于你提到的确切原因,这是一种反模式。您链接的问题有很多答案,有些确实建议使用同步上下文(包括mine)。
虽然任何给定的控件都可以在您尝试从发布的代理访问它时处理,但使用Control.IsDisposed
可以轻松解决(作为您的委托正在UI线程上执行,因此在运行时没有任何东西可以处理控件:
public partial class MyForm : Form
{
private readonly SynchronizationContext _context;
public MyForm()
{
_context = SynchronizationContext.Current
//...
}
private MethodOnOtherThread()
{
//...
_context.Post(status =>
{
// I think it's enough to check the form's IsDisposed
// But if you want to be extra paranoid you can test someLabel.IsDisposed
if (!IsDisposed) {someLabel.Text = newText;}
},null);
}
}