Winforms多线程UI操作的随机跨线程操作异常

时间:2009-08-13 20:29:30

标签: c# .net multithreading heisenbug

出于某种原因,这种安全的方法引发了一个典型的例外。

  

跨线程操作无效:   从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();
    }

[UPDATE2]
实施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 

[更新] 以下是问题完整性的堆栈跟踪。

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 

7 个答案:

答案 0 :(得分:17)

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

之前我被这个烧过:我的问题是我在UI线程上正确实例化了一个表单,但是直到数据从Web返回时我才Show()已在工作线程上运行的服务调用。情节是,在工作线程完成其工作时,在作为Handle检查的一部分访问它之前,没有人询问表单的InvokeRequired。 。所以我的后台工作者线程询问了表单:我需要InvokeRequired吗?然后,表单的InvokeRequired实现说:好吧,让我看一下我的Handle,这样我就可以看到我的内部hwnd创建了什么线程,然后我会看看你是否'在同一个线程上。然后Handle实现说:我还不存在,所以让我现在为自己创建一个hwnd。 (你会看到它的发展方向。请记住,我们仍然在后台线程中,无辜地访问InvokeRequired属性。)

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

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

祝你的heisenbug好运!

答案 1 :(得分:5)

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

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

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

答案 2 :(得分:3)

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

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

System.InvalidOperationException: Cross-thread operation not valid: Control 'cboMyDropDown' accessed from a thread other than the thread it was created on.
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.TextBox.ResetAutoComplete(Boolean force)
   at System.Windows.Forms.TextBox.Dispose(Boolean disposing)
   at OurApp.Controls.OurDropDownControl.Dispose(Boolean disposing)
   at 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为true,inCrossThreadSafeCall为false,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。不确定这是否是未来版本中的问题,但我认为无论如何我都会为后代写这个答案。

答案 3 :(得分:2)

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

答案 4 :(得分:1)

总而言之,您有一个私有实例方法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();
    }
}

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

答案 5 :(得分:1)

Nicholas Piasecki的回答为我解释了这个问题。我经常遇到这个奇怪的错误,我很欣赏它为什么会发生这样的信息(一个控件的句柄可能是在第一次调用this.InvokeRequired从后台线程中加载时很懒)

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

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

为什么这个工作,内置的Form.Dispose()方法我不知道。

无论如何,我在将来在工作线程附近创建控件时会更加小心,现在我知道Handles是懒惰的,所以谢谢!

答案 6 :(得分:0)

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

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

我从未见过这个问题。我知道msdn建议调用CreateControl而不是CreateHandle,但是,CreateControl没有为我剪切它。

有没有人知道是否有直接调用CreateHandle的副作用?