为啥 InvokeRequired 优于 WindowsFormsSynchronizationContext?

Posted

技术标签:

【中文标题】为啥 InvokeRequired 优于 WindowsFormsSynchronizationContext?【英文标题】:Why is InvokeRequired preferred over WindowsFormsSynchronizationContext?为什么 InvokeRequired 优于 WindowsFormsSynchronizationContext? 【发布时间】:2011-07-11 21:18:45 【问题描述】:

任何时候初学者问类似: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

另一个例子:假设fooTextBox。如果您关闭表单,并且在该非 GUI 线程执行 foo.InvokeRequired(这是错误的,因为表单已关闭)和 foo.AppendText 之后,它将导致 ObjectDisposedException

相比之下,在我看来,使用WindowsFormsSynchronizationContext 更容易——只有当线程仍然存在时才会使用Post 发布回调,如果线程不再存在,使用Send 的同步调用会抛出InvalidAsynchronousStateException

使用WindowsFormsSynchronizationContext 不是更简单吗?我错过了什么吗?如果 InvokeRequired-BeginInvoke 模式不是真正的线程安全,为什么要使用它?你觉得哪个更好?

【问题讨论】:

【参考方案1】:

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 仅当创建它的线程上仍有消息泵时才会执行委托。如果没有什么都没有发生并且没有引发异常。另外,如果稍后将另一个消息泵附加到线程(例如通过第二次调用Application.Run),委托将执行(这是因为系统为每个线程维护一个消息队列,但不知道有人是否从其中抽取消息) WindowsFormsSynchronizationContext.Send 将抛出 InvalidAsynchronousStateException 如果它绑定的线程不再活动。但是如果它绑定的线程是存活的并且没有运行消息循环它不会立即执行但是仍然会被放置在消息队列中并且如果再次执行Application.Run 就会被执行。

如果在自动释放的控件(如主窗体)上调用 IsDisposed,则这些情况都不会意外执行代码,因为即使在意外时间执行委托也会立即退出。

危险的情况是调用WindowsFormsSynchronizationContext.Send 并考虑到代码将被执行:它可能不会,现在有办法知道它是否做了任何事情。


我的结论是WindowsFormsSynchronizationContext 是一个更好的解决方案,只要正确使用。

它可以在复杂的情况下产生微妙的问题,但常见的 GUI 应用程序只有一个消息循环,只要应用程序本身一直正常运行,它就会一直存在。

【讨论】:

是的,但它会调用 BeginInvokeInvoke 一些魔法 controlToSendTo。另请参阅我对丹尼尔斯回答的评论。 我认为创建这个神奇的controlToSendTo 只是为了确保它存在并且在进程的生命周期内创建了句柄,因此InvalidOperationException 永远不会发生与手动调用@987654341 相比@ 控制,不能再有句柄。 是的,我没有看到它使用了空间编组控制。在这种情况下,它对调用很有用,您应该使用此信息做出完整的回答,因为这似乎是最大的不同。 编辑了我的答案,结果是这样。 事实上,如果由于控件已释放而未创建句柄,则标准示例代码中的问题会更严重... InvokeRequired 返回 false 并且该方法将继续操作已释放的控件。 .. 并不总是最好的做法,特别是取决于潜在抛出的异常最终会在哪里结束,它可能会使进程崩溃:D【参考方案2】:

谁说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);
    

【讨论】:

以上是关于为啥 InvokeRequired 优于 WindowsFormsSynchronizationContext?的主要内容,如果未能解决你的问题,请参考以下文章

为啥“实现可运行”优于“扩展线程”? [复制]

为啥模板构造函数优于复制构造函数?

为啥采用指针的函数优于采用数组引用的函数?

为啥 AJAX 优于 iFrame?

为啥 Bootstrap 网格布局优于 HTML 表格?

为啥 DCT 变换在视频/图像压缩中优于其他变换