Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A random cross-thread operation exception for Winforms multithreaded UI operation

For some reason, this safe-looking method raises a classic exception.

Cross-thread operation not valid: Control 'statusLabel' accessed from a thread other than the thread it was created on.

This code obviously should call an anonymous method through Invoke when invoke is required. But the exception occurs every once in a while.

Has anyone had a similar problem?

    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();
    }

Here is the stack-trace:

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 

I still get the same error after implementing 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 
like image 935
dance2die Avatar asked Aug 13 '09 20:08

dance2die


3 Answers

This may or may not be directly relevant to your situation, but could provide a clue. One important leaky abstraction to remember about Windows Forms is that a window Handle is not created until it is actually required. The Handle property only creates the real Windows hwnd on the first get call, which doesn't occur when a Control-derived object (like a Windows Form) is instanced. (The Control-derived object, after all, is just a .NET class.) In other words, it's a property that is lazily initialized.

I've been burned by this before: The problem in my case is that I had correctly instanced a form on a UI thread, but I was not Show()ing it until the data had come back from a web service invocation that had been operating on a worker thread. The scenario was that no one had asked for the form's Handle, ever, until it was accessed as part of the InvokeRequired check that occurred when the worker thread had completed its work. So my background worker thread asked the form: do I need InvokeRequired? The InvokeRequired implementation of the form then said: well, let me look at my Handle so I can see what thread my internal hwnd was created on, and then I'll see if you're on that same thread. And so then the Handle implementation said: well I don't yet exist, so let me create an hwnd for myself right now. (You see where this is going. Remember, we're still on the background thread, innocently accessing the InvokeRequired property.)

This resulted in the Handle (and its underlying hwnd) being created on the worker thread, which I did not own, and which didn't have a message pump set up to handle Windows messages. The result: my app locked up when other calls were made to the previously hidden window, as these calls were made on the main UI thread, which reasonably assumed that all other Control-derived objects had also been created on this thread. In other cases, this could cause strange cross-thread exceptions because InvokeRequired would return false unexpectedly, since the Handle was created on a thread that is different from the thread that the form was instanced on.

But only sometimes. I had functionality whereby the user could cause the form to Show() itself via a menu, and then it would disable itself while it populated itself with data in the background (showing a throbber animation). If they did this first, then everything would be okay: the Handle was created on the UI thread (in the menu item's event handler), and so InvokeRequired behaved as expected when the worker thread finished retrieving data from the Web service. But if my background thread that periodically ran (it was an events scheduler, similar to the Event Reminder dialog in Outlook) accessed the Web service and tried to pop up the form, and the user hadn't yet Show()n it, then the worker thread's touching InvokeRequired would cause the heartburn-inducing behavior described above.

Good luck with your heisenbug!

like image 139
Nicholas Piasecki Avatar answered Oct 19 '22 23:10

Nicholas Piasecki


Try overriding from Label, to create a new label class. Override the text property and place a breakpoint on it. Change the suspect label to use your new debug class instead. I've also found this technique is great for doing some basic profiling and/or debugging on your forms if you need to work out where and how things are being updated.

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

Use your code and try and catch heisenbug, you'll be able to break on every access to the label, so if it comes from a stacktrace which you don't expect and/or isn't from your invoke code path, you have your bug?

like image 5
Spence Avatar answered Oct 20 '22 00:10

Spence


Are you seeing this error when starting the application from the debugger, or when running stand-alone?

I have had the .NET Framework raise this exception incorrectly when running in the debugger. There is something peculiar about the debugger that causes the control's InvokeRequired flag incorrectly returns true, even when the code is running inside the main UI thread. It is very consitent for me, and it always happens after our control has been Disposed. Our stack trace looks like this:

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()

You can see the source of the error from the .NET Framework source code:

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;
            }
        }
}

When run in the debugger, checkForIllegalCrossThreadCalls is true, inCrossThreadSafeCall is false, and this.InvokeRequired is true despite being in the UI thread!

Note that Control.InvokeRequired winds up doing this:

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

Also note that our app uses the .NET Framework 2.0. Not sure if this is a problem in future versions, but I thought I'd write this answer anyway for posterity.

like image 3
Paul Williams Avatar answered Oct 19 '22 23:10

Paul Williams