【问题标题】:A random cross-thread operation exception for Winforms multithreaded UI operationWinforms多线程UI操作的随机跨线程操作异常
【发布时间】:2019-08-18 02:29:59
【问题描述】:

出于某种原因,这种看似安全的方法引发了一个典型的异常。

跨线程操作无效: 从 a 访问的控件“statusLabel” 线程以外的线程 创建于。

这段代码显然应该在需要调用时通过 Invoke 调用匿名方法。 但是这个异常每隔一段时间就会发生一次。

有人遇到过类似的问题吗?

    private void SetProgressBarValue(int progressPercentage)
    {
        Action setValue = () => 
        {
            var value = progressPercentage;
            if (progressPercentage < 0)
                value = 0;
            else if (progressPercentage > 100)
                value = 100;
            statusProgressBar.Value = value;
            statusLabel.Text = string.Format("{0}%", value);
        };
        if (InvokeRequired)
            Invoke(setValue);
        else
            setValue();
    }

这是堆栈跟踪:

at System.Windows.Forms.Control.get_Handle()
at System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
at System.Windows.Forms.ToolStrip.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
at System.Windows.Forms.ToolStrip.System.Windows.Forms.Layout.IArrangedElement.SetBounds(Rectangle bounds, BoundsSpecified specified)
at System.Windows.Forms.Layout.DefaultLayout.xLayoutDockedControl(IArrangedElement element, Rectangle newElementBounds, Boolean measureOnly, ref Size preferredSize, ref Rectangle remainingBounds)
at System.Windows.Forms.Layout.DefaultLayout.LayoutDockedControls(IArrangedElement container, Boolean measureOnly)
at System.Windows.Forms.Layout.DefaultLayout.xLayout(IArrangedElement container, Boolean measureOnly, ref Size preferredSize)
at System.Windows.Forms.Layout.DefaultLayout.LayoutCore(IArrangedElement container, LayoutEventArgs args)
at System.Windows.Forms.Layout.LayoutEngine.Layout(Object container, LayoutEventArgs layoutEventArgs)
at System.Windows.Forms.Control.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.ScrollableControl.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.Form.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.Control.PerformLayout(LayoutEventArgs args)
at System.Windows.Forms.Control.System.Windows.Forms.Layout.IArrangedElement.PerformLayout(IArrangedElement affectedElement, String affectedProperty)
at System.Windows.Forms.Layout.LayoutTransaction.DoLayout(IArrangedElement elementToLayout, IArrangedElement elementCausingLayout, String property)
at System.Windows.Forms.Control.PerformLayout(LayoutEventArgs args)
at System.Windows.Forms.Control.System.Windows.Forms.Layout.IArrangedElement.PerformLayout(IArrangedElement affectedElement, String affectedProperty)
at System.Windows.Forms.Layout.LayoutTransaction.DoLayout(IArrangedElement elementToLayout, IArrangedElement elementCausingLayout, String property)
at System.Windows.Forms.ToolStripItem.InvalidateItemLayout(String affectedProperty, Boolean invalidatePainting)
at System.Windows.Forms.ToolStripItem.OnTextChanged(EventArgs e)
at System.Windows.Forms.ToolStripItem.set_Text(String value)
at App.Image.Replace.ReplacementImageProcessForm.<>c__DisplayClass8.<SetProgressBarValue>b__7() in ReplacementImageProcessForm.cs: line 114
at App.Image.Replace.ReplacementImageProcessForm.SetProgressBarValue(Int32 progressPercentage) in ReplacementImageProcessForm.cs: line 119
at App.Image.Replace.ReplacementImageProcessForm.replacer_BeginReplace(Object sender, EventArgs e) in ReplacementImageProcessForm.cs: line 76
at App.Image.Replace.DocumentReplacer.OnBeginReplace() in IDocumentReplacer.cs: line 72
at App.Image.Replace.DocumentReplacer.Replace(Int32 documentId, String replacementDocumentPath) in IDocumentReplacer.cs: line 108 

实现John Saunders's suggestion后我仍然遇到同样的错误:

at System.Windows.Forms.Control.get_Handle()
at System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
at System.Windows.Forms.ToolStrip.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
at System.Windows.Forms.ToolStrip.System.Windows.Forms.Layout.IArrangedElement.SetBounds(Rectangle bounds, BoundsSpecified specified)
at System.Windows.Forms.Layout.DefaultLayout.xLayoutDockedControl(IArrangedElement element, Rectangle newElementBounds, Boolean measureOnly, ref Size preferredSize, ref Rectangle remainingBounds)
at System.Windows.Forms.Layout.DefaultLayout.LayoutDockedControls(IArrangedElement container, Boolean measureOnly)
at System.Windows.Forms.Layout.DefaultLayout.xLayout(IArrangedElement container, Boolean measureOnly, ref Size preferredSize)
at System.Windows.Forms.Layout.DefaultLayout.LayoutCore(IArrangedElement container, LayoutEventArgs args)
at System.Windows.Forms.Layout.LayoutEngine.Layout(Object container, LayoutEventArgs layoutEventArgs)
at System.Windows.Forms.Control.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.ScrollableControl.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.Form.OnLayout(LayoutEventArgs levent)
at System.Windows.Forms.Control.PerformLayout(LayoutEventArgs args)
at System.Windows.Forms.Control.System.Windows.Forms.Layout.IArrangedElement.PerformLayout(IArrangedElement affectedElement, String affectedProperty)
at System.Windows.Forms.Layout.LayoutTransaction.DoLayout(IArrangedElement elementToLayout, IArrangedElement elementCausingLayout, String property)
at System.Windows.Forms.Control.PerformLayout(LayoutEventArgs args)
at System.Windows.Forms.Control.System.Windows.Forms.Layout.IArrangedElement.PerformLayout(IArrangedElement affectedElement, String affectedProperty)
at System.Windows.Forms.Layout.LayoutTransaction.DoLayout(IArrangedElement elementToLayout, IArrangedElement elementCausingLayout, String property)
at System.Windows.Forms.ToolStripItem.InvalidateItemLayout(String affectedProperty, Boolean invalidatePainting)
at System.Windows.Forms.ToolStripItem.OnTextChanged(EventArgs e)
at System.Windows.Forms.ToolStripItem.set_Text(String value)
at App.Image.Replace.ReplacementImageProcessForm.<>c__DisplayClassa.<>c__DisplayClassc.<SetProgressBarValue>b__9() in ReplacementImageProcessForm.cs: line 147
at App.Image.Replace.ReplacementImageProcessForm.InvokeIfNecessary(Control control, Action setValue) in ReplacementImageProcessForm.cs: line 156
at App.Image.Replace.ReplacementImageProcessForm.<>c__DisplayClassa.<SetProgressBarValue>b__7() in ReplacementImageProcessForm.cs: line 145
at App.Image.Replace.ReplacementImageProcessForm.InvokeIfNecessary(Control control, Action setValue) in ReplacementImageProcessForm.cs: line 156
at App.Image.Replace.ReplacementImageProcessForm.SetProgressBarValue(Int32 progressPercentage) in ReplacementImageProcessForm.cs: line 132
at App.Image.Replace.ReplacementImageProcessForm.replacer_BeginReplace(Object sender, EventArgs e) in ReplacementImageProcessForm.cs: line 74
at App.Image.Replace.DocumentReplacer.OnBeginReplace() in IDocumentReplacer.cs: line 87
at App.Image.Replace.DocumentReplacer.Replace(Int32 documentId, String replacementDocumentPath) in IDocumentReplacer.cs: line 123 

【问题讨论】:

  • 您有没有三重检查过没有其他代码与statusLabel对话?
  • @Marc:除非 Resharper 4.5.1 是在撒谎,否则除了 InitializeComponent() 之外,这是唯一的地方
  • 嗯...这是个好问题...
  • 为了确定,您是否仔细检查了 Studios 的“查找所有参考资料”?
  • @Henk:是的。我什至关闭了 Resharper 插件只是为了使用内置的 VS 2008 的 Find all References to "triple-check" ;)

标签: c# .net multithreading


【解决方案1】:

这可能与您的情况直接相关,也可能不直接相关,但可以提供线索。关于 Windows 窗体要记住的一个重要的泄漏抽象是,在实际需要之前不会创建窗口 HandleHandle 属性仅在第一个 get 调用时创建真正的 Windows hwnd,当实例化 Control 派生对象(如 Windows 窗体)时不会发生这种情况。 (Control 派生对象毕竟只是一个 .NET 类。)换句话说,它是一个延迟初始化的属性。

我之前被这个问题困扰过:我的问题是我已经在 UI 线程上正确地实例化了一个表单,但直到数据从 Web 服务调用返回时我才Show()ing 它一直在工作线程上运行。这种情况是,从来没有人要求表单的Handle,直到它作为工作线程完成其工作时发生的InvokeRequired 检查的一部分被访问。于是我的后台工作线程问了表单:我需要InvokeRequired吗?然后表单的InvokeRequired 实现说:好吧,让我看看我的Handle,这样我就可以看到我的内部hwnd 是在哪个线程上创建的,然后我会看看你是否在同一个线程上.然后Handle 实现说:好吧,我还不存在,所以让我现在为自己创建一个hwnd。 (你知道这是怎么回事。记住,我们仍然在后台线程上,无辜地访问InvokeRequired 属性。)

这导致在 worker 线程上创建了Handle(及其底层hwnd),我不拥有该线程,也没有设置消息泵处理 Windows 消息。结果:当对先前隐藏的窗口进行其他调用时,我的应用程序被锁定,因为这些调用是在主 UI 线程上进行的,这合理地假设所有其他 Control 派生对象也已在此线程上创建。在其他情况下,这可能会导致奇怪的跨线程异常,因为 InvokeRequired 会意外返回 false,因为 Handle 是在与实例化表单的线程不同的线程上创建的。

但只是有时。我有这样的功能,用户可以通过菜单使表单本身Show(),然后它会在后台填充数据时禁用自身(显示颤动动画)。如果他们先这样做,那么一切都会好起来的:Handle 是在 UI 线程上创建的(在菜单项的事件处理程序中),所以当工作线程完成从 Web 服务检索数据时,InvokeRequired 的行为与预期一样.但是如果我定期运行的后台线程(它是一个事件调度程序,类似于 Outlook 中的事件提醒对话框)访问 Web 服务并尝试弹出表单,而用户还没有Show()n 它,那么工作线程接触InvokeRequired 会导致上述引起胃灼热的行为。

祝你的 heisenbug 好运!

【讨论】:

  • 谢谢,这帮助我解决了一个棘手的错误,我总是从另一个线程填充表单。 WinForms 很棘手!
  • 我自己刚遇到这个问题,不知道句柄不是在表单初始化时创建的。在我的例子中,我使用句柄来获取一个图形对象来测量字符串长度,这本身就很好。稍后显示表单有时会挂起整个应用程序,具体取决于此内部计时器是否在显示之前先触发。为此我把头撞在桌子上一个星期了!很好的解释,谢谢。
【解决方案2】:

尝试从标签覆盖,以创建一个新的标签类。覆盖 text 属性并在其上放置断点。更改可疑标签以改用新的调试类。如果您需要确定更新的位置和方式,我还发现这种技术非常适合对表单进行一些基本的分析和/或调试。

public class MyLabel : Label
{
    public override string Text
    {
        get
        {
            return base.Text;
        }
        set
        {
            base.Text = value;
        }
    }
}

使用您的代码并尝试捕获 heisenbug,您将能够中断对标签的每次访问,因此如果它来自您不期望的堆栈跟踪和/或不是来自您的调用代码路径,你有你的错误吗?

【讨论】:

  • -1:仔细查看堆栈跟踪。很明显它在做statusLabel.Text = string.Format("{0}%", value)
  • 1.在指定堆栈跟踪之前提供了答案,而不是如何将其标记为更新。 2. 向我解释为什么提供一种定位错误的技术是错误的。
  • +1: 无论如何,我和你在一起,Spence。我半随机地偶然发现了这个线程,并认为这个建议总体上是一个很好的提示。谢谢!
【解决方案3】:

您是否在从调试器启动应用程序时或在独立运行时看到此错误?

在调试器中运行时,.NET Framework 错误地引发了此异常。调试器有一些特殊的地方会导致控件的 InvokeRequired 标志错误地返回 true,即使代码在主 UI 线程中运行也是如此。这对我来说非常一致,并且总是在我们的控制权被处置之后发生。我们的堆栈跟踪如下所示:

System.InvalidOperationException:跨线程操作无效:控件“cboMyDropDown”从创建它的线程以外的线程访问。 在 System.Windows.Forms.Control.get_Handle() 在 System.Windows.Forms.TextBox.ResetAutoComplete(布尔力) 在 System.Windows.Forms.TextBox.Dispose(布尔处理) 在 OurApp.Controls.OurDropDownControl.Dispose(布尔处理) 在 System.ComponentModel.Component.Dispose()

你可以从.NET Framework源代码中看到错误的来源:

public class Control //...
{ //...
        public IntPtr Handle
        {
            get
            {
                if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
                {
                    throw new InvalidOperationException(System.Windows.Forms.SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
                }
                if (!this.IsHandleCreated)
                {
                    this.CreateHandle();
                }
                return this.HandleInternal;
            }
        }
}

在调试器中运行时,checkForIllegalCrossThreadCalls 为真,inCrossThreadSafeCall 为假,this.InvokeRequired 为真,尽管在 UI 线程中!

请注意,Control.InvokeRequired 最终会这样做:

int windowThreadProcessId = System.Windows.Forms.SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);
int currentThreadId = System.Windows.Forms.SafeNativeMethods.GetCurrentThreadId();
return (windowThreadProcessId != currentThreadId);

另请注意,我们的应用程序使用 .NET Framework 2.0。不确定这是否是未来版本中的问题,但我想我还是会为后代写下这个答案。

【讨论】:

  • @Paul:实际上,当我通过调试器运行时,我看到了这一点。但是在单机模式下没有遇到过这个问题。
  • 好的,那么这可能是一个类似的问题。在这种情况下,堆栈跟踪看起来不像 UI 线程。我没有在底部看到 Main 。你能验证这段代码是在工作线程中执行的吗?如果是这样,那么 InvokeRequired 应该 返回 true,并且应该调用回 UI 线程。此外,您可以尝试取出匿名委托并使用委托返回此方法。我严重怀疑匿名代表是问题所在,但排除它并没有什么坏处。
  • 保罗:要么 \
     要么使用按钮(缩进)
【解决方案4】:

尝试使用 BeginInvoke() 而不是 Invoke()。

【讨论】:

  • 这看起来值得一试,但我想知道这是否真的有必要只是为了解决可能的错误。
  • 你应该避免让其他线程等待 UI 线程做某事。 Invoke() 是对 UI 线程的同步调用,而 BeginInvoke() 是异步的。不管怎样,试一试吧。如果有帮助,请告诉我们。
【解决方案5】:

总而言之,您有一个私有实例方法SetProgressBarValue。它是控件或表单的实例方法。此控件或表单包含其他控件,statusProgressBarstatusLabel。因此,您实际上是在执行以下操作:

if (this.InvokeRequired)
{
    Invoke(
        (Action) delegate
                 {
                     statusProgressBar.Value = 0;                 // TOUCH
                     statusLabel.Text = string.Format("{0}%", 0); // TOUCH
                 });
}
else
{
    statusProgressBar.Value = 0;                                 // TOUCH
    statusLabel.Text = string.Format("{0}%", 0);                 // TOUCH
}

此代码假定如果 this.InvokeRequired == false,则这意味着 statusProgressBar.InvokeRequired == false 和 statusLabel.InvokeRequired == false。我建议你发现了一个不正确的情况。

尝试将代码更改为:

private void SetProgressBarValue(int progressPercentage)
{
    InvokeIfNecessary(
        this, () =>
              {
                  var value = progressPercentage;
                  if (progressPercentage < 0)
                  {
                      value = 0;
                  }
                  else if (progressPercentage > 100)
                  {
                      value = 100;
                  }

                  InvokeIfNecessary(
                      statusProgressBar.GetCurrentParent(),
                      () => statusProgressBar.Value = value);

                  InvokeIfNecessary(
                      statusLabel.GetCurrentParent(),
                      () =>
                      statusLabel.Text = string.Format("{0}%", value));
              });
}

private static void InvokeIfNecessary(Control control, Action setValue)
{
    if (control.InvokeRequired)
    {
        control.Invoke(setValue);
    }
    else
    {
        setValue();
    }
}

我怀疑您可能以某种方式导致这三个控件的窗口句柄在不同的线程上创建。我认为这段代码可以工作,即使所有三个窗口句柄都是在不同的线程上创建的。

【讨论】:

  • @John Saunders:谢谢你,约翰。我正在尝试你的方法,但由于我很少遇到有问题的错误,我会让它运行一段时间,看看它是如何工作的。顺便问一下,你介意我问一下,“我建议你发现了一个不正确的情况”在什么情况下成立?
  • 我不知道可能导致这种情况的特定情况。然而,一位智者曾经说过:“一旦你排除了不可能,剩下的,无论多么不可能,都是真理”。我同意。
  • 如果他使用多个 UI 线程,我会感到非常惊讶......我很想知道这是怎么回事。
  • @Spence:我希望我也能解决这个问题。我会继续更新这个问题,直到我深入了解这个问题。
  • @John:是的。我已经更新了问题并再次发布了堆栈跟踪。
【解决方案6】:

Nicholas Piasecki 的回答为我阐明了这个问题。我经常遇到这个奇怪的错误,我很欣赏关于它发生原因的信息(控件的句柄可能在第一次调用 this.InvokeRequired 从后台线程时被延迟加载)

我动态地创建了很多 UI(在 UI 线程上)并绑定到演示者(MVP 模式),这些演示者通常在 UI 首次显示之前启动工作线程。当然还有 UI 更新,这些更新使用 this.InvokeRequired/BeginInvoke 编组到 UI 线程,但是此时我假设可能会在工作线程上创建句柄。

对我来说,当用户关闭应用程序时,MainForm dispose 方法中发生了跨线程冲突。作为一种解决方法,当主窗体关闭时,我递归地遍历子控件处理它们及其子控件。然后减少我处置的控件列表,我最终将其缩小到导致访问冲突的单个控件。不幸的是,我无法直接解决问题(在有问题的控件上调用 CreateControl() 或 CreateHandle() 并没有解决问题),但我能够通过将递归处理留在应用程序上来解决问题关闭。

我不知道为什么它会起作用并且内置的 Form.Dispose() 方法不是。

无论如何,我以后在工作线程附近创建控件时会更加小心,现在我知道句柄是延迟加载的,所以谢谢!

【讨论】:

    【解决方案7】:

    我有一个类似的问题,我正在实例化一个表单,该表单启动一个后台线程来获取一些数据并在调用 Show() 之前更新自身。在此操作的第二个实例(总是)上,我会在 Show() 上得到一个跨线程异常。 在阅读了 Nicholas 的出色回答后,我在表单的构造函数中放置了一个断点并检查了返回 false 的 IsHandleCreated。然后我输入了这段代码:

            if (!this.IsHandleCreated)
                this.CreateHandle();
    

    从那以后我就再也没有看到过这个问题。我知道 msdn 建议调用 CreateControl 而不是 CreateHandle,但是,CreateControl 并没有为我剪掉它。

    有谁知道直接调用 CreateHandle 是否有任何副作用?

    【讨论】: