为什么 InvokeRequired 优于 WindowsFormsSynchronizationContext?

发布于 2024-10-21 01:30:05 字数 1163 浏览 0 评论 0原文

任何时候初学者问这样的问题: 如何从另一个更新 GUI 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 (这是 false,因为表单已关闭)和 foo.AppendText 它将导致 ObjectDisposeException.

相反,在我看来,使用 WindowsFormsSynchronizationContext 更容易 - 仅当线程仍然存在时,才会发生使用 Post 发布回调,如果线程不再存在,使用 Send 的同步调用会抛出 InvalidAsynchronousStateException

使用 WindowsFormsSynchronizationContext 不是更简单吗?我错过了什么吗?如果 InvokeRequired-BeginInvoke 模式并不是真正的线程安全,为什么我应该使用它?你认为什么更好?

Anytime the beginner asks something like: How to update the GUI from another thread in C#?, the answer is pretty straight:

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

But is it really good to use it? Right after non-GUI thread executes foo.InvokeRequired the state of foo can change. For example, if we close form right after foo.InvokeRequired, but before foo.BeginInvoke, calling foo.BeginInvoke will lead to InvalidOperationException: Invoke or BeginInvoke cannot be called on a control until the window handle has been created. This wouldn't happen if we close the form before calling InvokeRequired, because it would be false even when called from non-GUI thread.

Another example: Let's say foo is a TextBox. If you close form, and after that non-GUI thread executes foo.InvokeRequired (which is false, because form is closed) and foo.AppendText it will lead to ObjectDisposedException.

In contrast, in my opinion using WindowsFormsSynchronizationContext is much easier - posting callback by using Post will occur only if thread still exists, and synchronous calls using Send throws InvalidAsynchronousStateException if thread not exists anymore.

Isn't using WindowsFormsSynchronizationContext just easier? Am I missing something? Why should I use InvokeRequired-BeginInvoke pattern if it's not really thread safe? What do you think is better?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

つ低調成傷 2024-10-28 01:30:05

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。


如果您使用多个消息循环或多个 UI 线程,请注意 WindowsFormsSynchronizationContext.Post 的一些特殊情况:

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

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

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


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

在复杂的情况下,它可能会产生微妙的问题,但具有一个消息循环的常见 GUI 应用程序,只要应用程序本身存在,就总是没问题。

WindowsFormsSynchronizationContext works by attaching itself to a special control that is bound to the thread where the context is created.

So

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

Can be replaced with a safer version :

context.Post(delegate
{
    if (foo.IsDisposed) return;
    ...
});

Assuming that context is a WindowsFormsSynchronizationContext created on the same UI thread that foo was.

This version avoid the problem you evoke :

Right after non-GUI thread executes foo.InvokeRequired the state of foo can change. For example, if we close form right after foo.InvokeRequired, but before foo.BeginInvoke, calling foo.BeginInvoke will lead to InvalidOperationException: Invoke or BeginInvoke cannot be called on a control until the window handle has been created. This wouldn't happen if we close the form before calling InvokeRequired, because it would be false even when called from non-GUI thread.


Beware of some special cases with WindowsFormsSynchronizationContext.Post if you play with multiple message loops or multiple UI threads :

  • WindowsFormsSynchronizationContext.Post will execute the delegate only if there still is a message pump on the thread where it was created. If there isn't nothing happens and no exception is raised.
    Also if another message pump is later attached to the thread (Via a second call to Application.Run for example) the delegate will execute (It's due to the fact that the system maintain a message queue per thread without any knowledge about the fact that someone is pumping message from it or not)
  • WindowsFormsSynchronizationContext.Send will throw InvalidAsynchronousStateException if the thread it's bound to isn't alive anymore. But if the thread it's bound to is alive and doesn't run a message loop it won't be executed immediately but will still be placed on the message queue and executed if Application.Run is executed again.

None of these cases should execute code unexpectedly if IsDisposed is called on a control that is automatically disposed (Like the main form) as the delegate will immediately exit even if it's executed at an unexpected time.

The dangerous case is calling WindowsFormsSynchronizationContext.Send and considering that the code will be executed: It might not, and there is now way to know if it did anything.


My conclusion would be that WindowsFormsSynchronizationContext is a better solution as long as it's correctly used.

It can create sublte problems in complex cases but common GUI applications with one message loop that live as long as the application itself will always be fine.

朱染 2024-10-28 01:30:05

谁说 InvokeRequired / Control.BeginInvoke 是首选?如果你问我,在大多数情况下,由于你提到的确切原因,这是一种反模式。您链接到的问题有很多答案,有些实际上确实建议使用同步上下文(包括我的)。

虽然任何给定的控件确实可以在您尝试从发布的委托访问它时被释放,但这可以使用 Control.IsDispose (当您的委托在 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);
    }
}

Who said InvokeRequired / Control.BeginInvoke is preferred? If you ask me, in most cases it's an anti pattern for the exact reasons you mentioned. The question you linked to has many answers, and some actually do suggest using the synchronization context (including mine).

While it's true that any given control could be disposed by the time you're trying to access it from the posted delegate, that's easily solved using Control.IsDisposed (as your delegate is executing on the UI thread so nothing can dispose controls while it's running):

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);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文