为什么InvokeRequired比WindowsFormsSynchronizationContext更受青睐?

7

当初学者问类似于:如何在C#中从另一个线程更新GUI?时,答案很简单:

if (foo.InvokeRequired)
{
    foo.BeginInvoke(...)
} else {
    ...
}

但是真的适合使用吗?在非GUI线程执行 foo.InvokeRequired 后,foo 的状态可能会发生改变。例如,如果我们在调用 foo.BeginInvoke 之前关闭表单,调用 foo.BeginInvoke 将导致 InvalidOperationException直到窗口句柄被创建之前,不能在控件上调用 Invoke 或 BeginInvoke。 如果我们在调用 InvokeRequired 之前关闭表单,这种情况就不会发生,因为即使从非GUI线程调用时,它也将是 false
相比之下,我认为使用 WindowsFormsSynchronizationContext 更容易 - 使用 Post 发布回调仅会在线程仍然存在时发生,并且使用 Send 进行同步调用,如果线程不再存在,则会抛出 InvalidAsynchronousStateException
难道使用 WindowsFormsSynchronizationContext 不是更简单吗?我漏看了什么吗?如果 InvokeRequired-BeginInvoke 模式实际上不是线程安全的,为什么要使用它?你认为哪个更好?
2个回答

7

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线程调用它时,它也将为false,因此不会发生这种情况。
注意使用WindowsFormsSynchronizationContext.Post时一些特殊情况,如果您使用多个消息循环或多个UI线程: WindowsFormsSynchronizationContext.Post仅在创建它的线程上仍然存在消息泵时才执行委托。如果没有任何事情发生,也不会引发异常。此外,如果稍后将另一个消息泵附加到该线程(例如通过第二次调用Application.Run),则将执行委托(这是由于系统在每个线程上维护一个消息队列,而不知道是否有人正在从中抽气)。 WindowsFormsSynchronizationContext.Send将抛出InvalidAsynchronousStateException,如果它绑定到的线程已经不存在了。但是,如果它绑定到的线程仍然存在且没有运行消息循环,则不会立即执行,但仍将被放置在消息队列中,并在再次执行Application.Run时执行。
如果对自动处理的控件(如主窗体)调用IsDisposed,则这些情况都不应意外执行代码,因为即使在意外时间执行委托,它也会立即退出。
危险的情况是调用WindowsFormsSynchronizationContext.Send并考虑代码将被执行:它可能不会执行,而且无法知道它是否执行了任何操作。
我的结论是,在正确使用的情况下,WindowsFormsSynchronizationContext是更好的解决方案。
在复杂情况下,它可能会创建微妙的问题,但具有一个消息循环并且与应用程序本身一样长寿的常见GUI应用程序始终是可以的。

1
实际上,如果句柄未被创建而控件已被处理,则标准示例代码中的问题甚至更糟... InvokeRequired 会返回 false,方法将继续操作一个已被处理的控件... 这并不总是最好的做法,特别是根据潜在抛出的异常可能结束的位置,它可能会导致进程崩溃:D - Julien Roncaglia
因此,如果您使用WindowsFormsSynchronizationContext,则有可能出现这样一种情况:您在已经被处理很久的对象上调用委托,而该对象没有任何句柄。这就是Control.IsDisposed的作用(https://msdn.microsoft.com/en-us/library/system.windows.forms.control.isdisposed(v=vs.110).aspx)。 - Ohad Schneider
@OhadSchneider,你需要调用此属性的控件未公开。它必须是用作参考的控件。您可以通过反射访问它,使用 Application.ThreadContext.FromCurrent().MarshalingControl 或访问 WindowsFormsSynchronizationContext 成员 controlToSendTo,但这有点繁琐。 - Julien Roncaglia
@JulienRoncaglia,你不需要检查那个控件,只要有UI线程来运行你的委托,它就会一直存在(基本上它的工作就是在那里)。 - Ohad Schneider
@OhadSchneider,是的,我读你的评论太快了。我完整地重新阅读了我的原始答案,但我也没有完全理解它... Control.isDisposed 可以工作。我看不到我5年前所说的问题XD 我会编辑它。 - Julien Roncaglia
显示剩余7条评论

0

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接