使用 Control.BeginInvoke() 调用异步方法时如何防止警告 VSTHRD101?

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

我正在重构一些旧的 WinForms 代码以使用 async/await,我遇到了代码分析警告,我不确定如何正确修复。

情况是我有一个从非UI线程调用的方法需要更新UI。旧代码使用历史悠久的技术调用 Control.BeginInvoke() 到 运行 UI 线程上的代码。

问题是我现在要调用的方法是 async 并且尝试使用 Control.BeginInvoke() 会产生警告 VSTHRD101:“避免将异步 lambda 用于返回无效的委托类型,因为任何未被委托处理的异常都会导致进程崩溃。"

以下示例 WinForms 应用程序演示了该问题。这是一个简单的表单,仅包含一个名为“testBtn”的 Button 和一个名为“label1”的 Label

notRunningOnUiThread() 方法中调用 ControlBeginInvoke() 会导致警告,如评论所述:

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

如果我抑制警告,即使 mustRunOnUiThreadAsync() 中有异常,一切似乎都 运行 正常(如果有异常,它会立即报告,而不是来自任务的终结器)。

我的问题是: 禁止显示此警告是否安全?如果不是,有什么好的修复方法?

我要解决的根本问题是如何从非[=64]线程安全地调用UI线程上的异步UI方法=] WinForms 应用程序中的线程,当只有 ui Form 可用且当前同步上下文不可用时。

(请注意,我不能在 notRunningOnUiThread() 中简单地 await mustRunOnUiThreadAsync()。那会导致抛出“跨线程操作无效”InvalidOperationException。)

我想一个可能的解决方案是使用 the advice from this answer 编写调用异步方法的非异步方法。但这是针对这种特殊情况的最佳方法吗?


[编辑]

我应该提到:在我的实际代码中,notRunningOnUiThread() 不是需要更新 UI 的 Control 的成员。因为 notRunningOnUiThread() 不是 运行 在 UI 线程上,所以我不能使用 System.Threading.SynchronizationContext,因为它是错误的上下文。但是,我确实有 Control 可用于调用 Control.BeginInvoke()

实际情况是我正在为一个 RPC 请求提供服务,该请求必须使用恰好是异步的方法更新一些 UI。

一个想法可能是将 mustRunOnUiThreadAsyncasync Task 转换为 async void。从概念上讲,您可以说此方法是源自后台线程的“事件”的“事件处理程序”,因此您基本上没有违反 Microsoft's guidelines 关于 async void 的用法。示例:

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

...并且在 notRunningOnUiThread 方法中:

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

此修改不会以任何方式改变您当前代码的行为,除了使 VSTHRD101 警告消失。

作为旁注,您还可以考虑将 testBtn_Click 处理程序转换为 async void,这样 notRunningOnUiThread 就不会作为即发即弃 Task:

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

感谢 Theodor Zoulias 的回答,我想我现在已经充分理解警告,知道我可以安全地抑制它。

这是因为委托 运行 的整个代码在 UI 消息循环的上下文中是 运行(因为 .BeginInvoke() 调用) 将报告任何异常。

这就是我的结局:

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

或者,您可以将方法更改为 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

这会将警告移至 MustRunOnUiThread(),因此并没有太大帮助,尽管与 VSTHRD101 相比,该警告可能更广为人知。

请注意以下内容不起作用,因为它没有观察到任何异常(并且它也没有给出代码分析警告!):

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

总结一下:

抑制针对特定情况的警告是安全的,因为可能抛出异常的代码在 UI 消息循环的上下文中是 运行,这将报告任何异常。