How to prevent warning VSTHRD101 when using Control.BeginInvoke() to call an async method?

Viewed 553

I'm refactoring some old WinForms code to use async/await, and I've hit a code analysis warning that I'm not sure how to fix properly.

The situation is that I have a method that is called from a non-UI thread which needs to update the UI. The old code uses the time-honoured technique of calling Control.BeginInvoke() to run the code on the UI thread.

The problem is that the method I want to invoke is now async and attempting to use Control.BeginInvoke() yields the warning VSTHRD101: "Avoid using async lambda for a void returning delegate type, because any exceptions not handled by the delegate will crash the process."

The following sample WinForms application demonstrates the problem. It's a simple form containing only a Button called "testBtn" and a Label called "label1".

The call to ControlBeginInvoke() inside the notRunningOnUiThread() method causes the warning as commented:

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (disabled for brevity in this sample code)

namespace Net5WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = true; // For test purposes to prove that mustRunOnUiThreadAsync() runs on the UI thread.
        }

        // This method is only to simulate what's happening in the real code. 
        // Assume that you cannot change this.
        void testBtn_Click(object sender, EventArgs e)
        {
            _ = Task.Run(() => notRunningOnUiThread(this)); // Not my real code, but this simulates it.
        }

        // NOTE: In the real code this method is not a member of the Control,
        // but it does have access to the Control to update.
        void notRunningOnUiThread(Control ui) // Called from a non-UI thread and wants to call a method that updates the UI.
        {
            // The next line causes warning VSTHRD101:
            // "Avoid using async lambda for a void returning delegate type,
            // because any exceptions not handled by the delegate will crash the process."

            ui.BeginInvoke(new Action(async () => await mustRunOnUiThreadAsync()));
        }

        async Task mustRunOnUiThreadAsync()
        {
            label1.Text = "Before update";
            await Task.Delay(1000).ConfigureAwait(true);
            label1.Text = "After update";
        }
    }
}

If I suppress the warning, everything seems to run OK even if there are exceptions in mustRunOnUiThreadAsync() (if there's an exception, it's reported immediately and not from the finalizer for a Task).

My question is: Is it safe to suppress this warning? And if not, what's a good way to fix it?

The fundamental thing I want to solve is how to safely call an async UI method on the UI thread from a non-UI thread in a WinForms application, when only the ui Form is available and the current synchronisation context is not available.

(Note that I can't simply await mustRunOnUiThreadAsync() in notRunningOnUiThread(). That would cause a "Cross-thread operation not valid" InvalidOperationException to be thrown.)

I suppose one possible solution is to write a non-async method that calls the async method, using the advice from this answer. But is that the best approach for this particular case?


[EDIT]

I should have mentioned: In my actual code the notRunningOnUiThread() is not a member of the Control that needs to update the UI. Because notRunningOnUiThread() isn't running on a UI thread, I can't use System.Threading.SynchronizationContext since it is the wrong context. I do have the Control available for calling Control.BeginInvoke() however.

The actual situation is that I am servicing an RPC request which must update some UI using a method that happens to be async.

2 Answers

One idea could be to convert the mustRunOnUiThreadAsync from async Task to async void. Conceptually you could say that this method is the "event handler" of an "event" that is originated from the background thread, and so you are not violating Microsoft's guidelines about async void usage essentially. Example:

async void mustRunOnUiThread()
{
    label1.Text = "Before update";
    await Task.Delay(1000).ConfigureAwait(true);
    label1.Text = "After update";
}

...and in the notRunningOnUiThread method:

ui.BeginInvoke(new Action(() => mustRunOnUiThread()));

This modification will not change the behavior of your current code in any way, except from making the VSTHRD101 warning go away.

As a side note you could also consider converting the testBtn_Click handler to async void, so that the notRunningOnUiThread is not launched as a fire-and-forget Task:

async void testBtn_Click(object sender, EventArgs e)
{
    await Task.Run(() => notRunningOnUiThread(this));
}

Thanks to Theodor Zoulias's answer, I think that I now understand the warning sufficiently to know that I can safely suppress it.

This is because the entirety of the code being run by the delegate is run in the context of the UI message loop (because of the .BeginInvoke() call) which will report any exceptions.

So this is what I end up with:

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Net5WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = true; // For test purposes to prove that mustRunOnUiThreadAsync() runs on the UI thread.
        }

        void testBtn_Click(object sender, EventArgs e)
        {
            _ = Task.Run(() => notRunningOnUiThread(this)); // Not my real code, but this simulates it.
        }

        static void notRunningOnUiThread(Form1 ui) // Called from a non-UI thread and wants to call a method that updates the UI.
        {
            #pragma warning disable VSTHRD101 // Avoid unsupported async delegates
            ui.BeginInvoke(new Action(async () => await ui.MustRunOnUiThreadAsync().ConfigureAwait(false)));
            #pragma warning restore VSTHRD101 // Avoid unsupported async delegates
        }

        public async Task MustRunOnUiThreadAsync()
        {
            label1.Text = "Before update";
            await Task.Delay(1000).ConfigureAwait(true);
            label1.Text = "After update";
            throw new InvalidOperationException("TEST"); // Check that this is reported.
        }
    }
}

Alternatively, you can change the method to async void:

static void notRunningOnUiThread(Form1 ui) // Called from a non-UI thread and wants to call a method that updates the UI.
{
    ui.BeginInvoke(new Action(ui.MustRunOnUiThread));
}

#pragma warning disable VSTHRD100 // Avoid async void methods
public async void MustRunOnUiThread()
{
    label1.Text = "Before update";
    await Task.Delay(1000).ConfigureAwait(true);
    label1.Text = "After update";
    throw new InvalidOperationException("TEST"); // Check that this is reported.
}
#pragma warning restore VSTHRD100 // Avoid async void methods

This moves the warning to MustRunOnUiThread() so it doesn't help much, although that warning is probably more widely understood compared to VSTHRD101.

Note that the following DOES NOT WORK because it doesn't observe any exception (and it doesn't give a Code Analysis warning either!):

static void notRunningOnUiThread(Form1 ui) // Called from a non-UI thread and wants to call a method that updates the UI.
{
    ui.BeginInvoke(new Action(() => ui.MustRunOnUiThreadAsync().ConfigureAwait(false))); // BAD CODE! Misses exceptions.
}

public async Task MustRunOnUiThreadAsync()
{
    label1.Text = "Before update";
    await Task.Delay(1000).ConfigureAwait(true);
    label1.Text = "After update";
    throw new InvalidOperationException("TEST"); // Check that this is reported.
}

So to summarise:

It's safe to suppress the warning for that particular case because the code that might thrown an exception is run in the context of the UI's message loop, which will report any exceptions.

Related