为什么InvokeRequired比WindowsFormsSynchronizationContext更受欢迎?
任何时候初学者都会问: 如何从C#中的另一个线程更新GUI? ,答案很简单:
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
同步调用Send
抛出InvalidAsynchronousStateException
。
是不是更容易使用WindowsFormsSynchronizationContext
? 我错过了什么吗? 如果它不是真的线程安全,为什么我应该使用InvokeRequired-BeginInvoke模式? 你觉得哪个更好?
WindowsFormsSynchronizationContext
通过将自身附加到绑定到创建上下文的线程的特殊控件来工作。
所以
if (foo.InvokeRequired) { foo.BeginInvoke(...) } else { ... }
可以用更安全的版本替换:
context.Post(delegate { if (foo.IsDisposed) return; ... });
假设context
是在foo
所在的同一UI线程上创建的WindowsFormsSynchronizationContext
。
此版本避免了您提出的问题:
在非GUI线程执行foo.InvokeRequired之后,foo的状态可以改变。 例如,如果我们在foo.InvokeRequired之后关闭表单,但在foo.BeginInvoke之前,调用foo.BeginInvoke将导致InvalidOperationException:在创建窗口句柄之前,无法在控件上调用Invoke或BeginInvoke。 如果我们在调用InvokeRequired之前关闭表单,则不会发生这种情况,因为即使从非GUI线程调用它也是错误的。
如果您使用多个消息循环或多个UI线程,请注意WindowsFormsSynchronizationContext.Post
的一些特殊情况:
- 只有在创建它的线程上仍有消息泵时,
WindowsFormsSynchronizationContext.Post
才会执行该委托。 如果没有任何反应,则不会引发exception 。
此外,如果稍后将另一个消息泵附加到该线程(例如,通过对Application.Run
的第二次调用),则委托将执行(这是因为系统维护每个线程的消息队列而不知道某人的事实是否从中抽取信息) - 如果绑定的线程不再存在,
WindowsFormsSynchronizationContext.Send
将抛出InvalidAsynchronousStateException
。 但是如果它绑定的线程是活动的并且没有运行消息循环,它将不会立即执行但仍将被放置在消息队列中并且如果再次执行Application.Run
则执行。
如果在自动处理的控件(如主窗体)上IsDisposed
则这些情况都不会意外执行代码,因为委托将立即退出,即使它是在意外时间执行的。
危险的情况是调用WindowsFormsSynchronizationContext.Send
并考虑代码将被执行:它可能没有,现在有办法知道它是否做了什么。
我的结论是WindowsFormsSynchronizationContext
是一个更好的解决方案,只要它被正确使用。
它可以在复杂的情况下创建sublte问题但是常见的GUI应用程序只有一个消息循环,只要应用程序本身一直很好。
谁说InvokeRequired
/ Control.BeginInvoke
是首选? 如果你问我,在大多数情况下,由于你提到的确切原因,这是一种反模式。 您链接的问题有很多答案,有些确实建议使用同步上下文(包括我的 )。
虽然任何给定的控件都可以在您尝试从发布的委托访问它时进行处理,但使用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); } }