2

I'm coming from WPF and I am new to WinForms. While investigating a cross-threading situation, a cross-thread exception that I expected did not occur.

Here is a basic summary of my situation. There is one Label control named label1 and one Button named button1. The click event handler for button1 essentially looks like this:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(()=>
    {
        label1.Text = "Some-Other-New-Text";
    });
}

This is not throwing a cross-thread exception as I expect it to. Is this because WinForms applications do not have cross-threading problems? Note that I have investigated this in Visual Studio 2013 as well as in Visual Studio 2010.

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Won Hyoung Lee
  • 383
  • 2
  • 14
  • 1
    @Sometimes it works, sometimes it doesnt. Cross Thread exceptions are not consistent. – CathalMF Apr 26 '16 at 09:21
  • Maybe `Control.CheckForIllegalCrossThreadCalls` was somehow set to `false`? – René Vogt Apr 26 '16 at 09:22
  • Nor does it change the label text. The exception seems to be thrown on the thread within the function defined in StartNew. – user6144226 Apr 26 '16 at 09:33
  • Try this [How to read combobox from a thread other than the thread it was created on?](http://stackoverflow.com/questions/5516111/how-to-read-combobox-from-a-thread-other-than-the-thread-it-was-created-on) – Jirawat Jannet Apr 26 '16 at 09:45
  • Might be related: http://stackoverflow.com/questions/25691114/where-does-an-async-task-throw-exception-if-it-is-not-awaited – user6144226 Apr 26 '16 at 09:45
  • 1
    @user6144226 no, label text is changed. i said it's work. – Won Hyoung Lee Apr 26 '16 at 10:22
  • 1
    @JirawatJannet I know Control.Invoke, I just ask why it doesn't occur exception – Won Hyoung Lee Apr 26 '16 at 10:23
  • @user6144226 Might be NOT related. if I use just Thread not Task, it return same result to me. label text is changed. – Won Hyoung Lee Apr 26 '16 at 10:26
  • @RenéVogt The flag is false. I just said It's work. I didn't said it's not work. If exception is occurred in background thread, just the thread is killed not program, I know it. I ask why there is no error. but every comment say perfectly different subject. – Won Hyoung Lee Apr 26 '16 at 10:32
  • @WonHyoungLee I double checked it. The code does indeed run like you say outside of a debugger: http://stackoverflow.com/questions/3972727/why-is-cross-thread-operation-exception-not-thrown-while-running-exe-in-bin-debu – user6144226 Apr 26 '16 at 10:46
  • 1
    @WonHyoungLee Not sure if I understand you correctly. If the flag is false, then of course it works without any exception and the label is changed. If the flag is `true`, the program _checks for illegal cross thread calls_ and the only strange thing is that the exception kills the thread without being bubbled up anywhere. – René Vogt Apr 26 '16 at 10:47
  • @RenéVogt Thanks, useful reply. Then, If the `Control.CheckForIllegalCrossThreadCalls` is false, do we need not to call `Control.Invoke()` in cross-thread situation? why my IDE set false as default? Anyway, Thank you very much. – Won Hyoung Lee Apr 26 '16 at 12:48
  • 2
    I'm suprised too that it is (sometimes? ) false per default. I strongly recommend to set it to true and ensure to update ui elements only from the ui thread. – René Vogt Apr 26 '16 at 12:50
  • @user6144226 It return same result when execute release compiled *.exe file in my case. I just try to force to set true as René Vogt's suggestion. – Won Hyoung Lee Apr 26 '16 at 13:57
  • 1
    @RenéVogt It's set to `Debugger.IsAttached` by default as per the latest reference source. So it crashes in a debugger, but "works" outside of a debugger. – Luaan May 02 '16 at 11:15

2 Answers2

2

Windows forms work on top of the Windows messaging infrastructure. That means that a lot of the operations you perform on the controls in question are actually delegated to Windows to support proper native behaviour of all the controls.

Label doesn't change the default implementation, and it doesn't cache the text in managed code by default. This means that it uses the SetWindowText native method to set the current label text (and correspondingly, GetWindowText to read it), which posts a WM_SETTEXT to the message loop. The real update happens on the thread that handles the message loop, also known as the UI thread. Unless you go out of your way to prohibit this kind of call (Control.checkForIllegalCrossThreadCall in current reference source), it will work. By default, this is set depending on whether a debugger is attached - so your code may crash while debugging, but will work outside of a debugger, since SetWindowText happens to be thread-safe. There's other parts of the Text property that may or may not be thread-safe, but if you're lucky, everything works just fine.

You can set Control.CheckForIllegalCrossThreadCall to true explicitly, and I'd recommend you to do so. Accessing any resource from multiple threads is prone to hard to debug issues, and marshalling whatever work needs to be done on the UI to the UI thread is... kind of the job of the UI thread anyway.

Manipulating the UI exclusively from the UI thread gives you quite important benefits:

  • Predictability and realiability - things will tend to happen in certain reliable orders. If I have a hundred threads setting the Textof two different controls, delegating the UI update to the UI thread will ensure that the two controls always have consistent values, while updating directly from the background threads will tend to interleave the updates "randomly". In a real application, this can cause confusion as well as hard to find bugs. Note that this isn't absolute - any await/Application.DoEvents may or may not disrupt this. But even in that case, you have well defined synchronization points, rather than preëmptive multi-tasking.
  • Multi-threading is hard. Most things you work with aren't thread-safe, and even allegedly thread-safe operations may have multi-threading bugs or simply complicated behaviour when running in a MT scenario. Even a simple thing like updating two booleans in a sequence becomes dangerous, and may introduce hard to debug bugs. Your best bet is to keep as many things thread-affine as you can. You'll usually find you can confine all interfaces between the threads to a tiny portion of your code, which makes them much easier to test and debug.
  • It's really cheap when you've already separated "work" from "UI" anyway. And that's a pretty handy design practice on its own.

As a side-note, you usually want to await tasks that you spin off, or handle exceptions explicitly. If you enable Control.CheckForIllegalCrossThreadCall, the label will no longer update, but it will not show an exception either - by default, threadpool threads ignore unhandled exceptions nowadays (since .NET 4.5.2 IIRC). The await will marshall any exceptions (and return values) back to the UI thread.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • "By default, this is set depending on whether a debugger is attached - so your code may crash while debugging, ..." But `dataGridView1.Rows[0].Cells[0].Value = 12345;` does not crash in the threaded task while debugging with even all options turned on. Microsoft says [The InvalidOperationException always occurs for unsafe cross-thread calls during Visual Studio debugging,](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls?view=netframeworkdesktop-4.8&redirectedfrom=MSDN#unsafe-cross-thread-calls) but it does not always. – SHIN JaeGuk Aug 13 '23 at 14:22
  • @SHINJaeGuk Yeah, DataGridView isn't a native control. If windows messages aren't involved, it's not an unsafe cross-thread call. Though of course, most objects aren't thread-safe, so you need to ensure proper synchronization is in place. In general, keeping all the UI access on the UI thread is still a good idea, if you can afford it. – Luaan Aug 14 '23 at 05:55
1

In my situation, I expected a cross-thread exception (in the debugger) in my WinForms application when accessing a ToolStripStatusLabel from a thread other than the UI thread.

I discovered that the reason that the cross-thread exception does not occur is because ToolStripStatusLabel does not derive from System.Windows.Forms.Control and therefore does not have a CheckForIllegalCrossThreadCalls property.

(I was able to narrow this down by first proving that attempting to access a TextBox in a non-UI thread did raise the cross-thread exception.)


† See this answer as to why WinForms cross-thread exceptions typically happen only when you are debugging.

DavidRR
  • 18,291
  • 25
  • 109
  • 191