从异步方法更新 GUI

Updating GUI from async method

在使用 async/await 创建简单示例的过程中,我发现,一些示例只是说明 Button1_Click 类方法的模式,并直接从 async 方法自由更新 GUI 控件。因此,可以将此视为一种安全机制。但是我的测试代码在 mscorlib.dll 中的 TargetInvocationException 异常上不断崩溃,内部异常如:NullReferenceArgumentOutOfRange 等。关于堆栈跟踪,一切似乎都指向 WinForms.StatusStrip 标签显示结果(并直接由绑定到按钮事件处理程序的 async 方法驱动)。在访问 GUI 控件时使用旧学校 Control.Invoke 时,崩溃似乎已修复。

问题是:我错过了什么重要的事情吗?异步方法是否与以前用于长期操作的 threads/background 工作人员一样安全,因此 Invoke 是推荐的解决方案?直接从 async 方法驱动 GUI 的代码片段是否错误?

example

编辑: 对于缺少来源的反对者: 创建一个包含三个按钮和一个包含两个标签的 StatusStrip 的简单表单...

//#define OLDSCHOOL_INVOKE

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

namespace AsyncTests
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }


        private async void LongTermOp()
        {
            int delay;
            int thisId;

            lock (mtx1)
            {
                delay  = rnd.Next(2000, 10000);
                thisId = firstCount++;
#if OLDSCHOOL_INVOKE
                Invoke(new Action(() =>
#endif
                label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
                ))
#endif
                ;
                ++firstPending;
            }

            await Task.Delay(delay);

            lock (mtx1)
            {
                --firstPending;
#if OLDSCHOOL_INVOKE
                Invoke(new Action(() =>
#endif
                label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
                ))
#endif
                ;
            }
        }


        private async Task LongTermOpAsync()
        {
            await Task.Run((Action)LongTermOp);
        }

        private readonly Random rnd  = new Random();
        private readonly object mtx1 = new object();
        private readonly object mtx2 = new object();
        private int firstCount;
        private int firstPending;
        private int secondCount;
        private int secondPending;

        private async void buttonRound1_Click(object sender, EventArgs e)
        {
            await LongTermOpAsync();
        }

        private async void buttonRound2_Click(object sender, EventArgs e)
        {
            await Task.Run(async () => 
            {
                int delay;
                int thisId;

                lock (mtx2)
                {
                    delay = rnd.Next(2000, 10000); 
                    thisId = secondCount++;
#if OLDSCHOOL_INVOKE
                    Invoke(new Action(() =>
#endif
                    label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
                    ))
#endif
                    ;
                    ++secondPending;
                }
                await Task.Delay(delay);
                lock (mtx2)
                {
                    --secondPending;
#if OLDSCHOOL_INVOKE
                    Invoke(new Action(() =>
#endif
                    label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
                    ))
#endif
                    ;
                }
            });            
        }

        private void buttonRound12_Click(object sender, EventArgs e)
        {
            buttonRound1_Click(sender, e);
            buttonRound2_Click(sender, e);
        }


        private bool isRunning = false;

        private async void buttonCycle_Click(object sender, EventArgs e)
        {
            isRunning = !isRunning;

            await Task.Run(() =>
            {
                while (isRunning)
                {
                    buttonRound12_Click(sender, e);
                    Application.DoEvents();
                }
            });
        }
    }
}

由于您使用的是异步方法,我的猜测是您尝试执行的代码不在 UI 线程上。

看这里: SO Question

Taskawait在这方面都不给您任何保证。您需要考虑创建任务的上下文以及发布延续的上下文。

如果您在 winforms 事件处理程序中使用 await,同步上下文将被捕获,并且继续 returns 返回到 UI 线程(事实上,它几乎在给定的代码块上调用 Invoke)。但是,如果您只是使用 Task.Run 开始一个新任务,或者您从另一个同步上下文 await 开始,这将不再适用。解决方案是 运行 在您可以从 winforms 同步上下文中获得的正确任务调度程序上继续。

但是,需要注意的是,这并不一定意味着 async 事件会正常运行。例如,Winforms 也将事件用于诸如 CellPainting 之类的事情,它实际上同步地依赖于它们 运行ning。如果您在这种情况下使用 await,它几乎可以保证不会正常工作 - 继续将仍然发布到 UI 线程,但这并不一定保证安全。例如,假设控件有这样的代码:

using (var graphics = NewGraphics())
{
  foreach (var cell in cells)
    CellPainting(cell, graphics);
}

在您继续 运行 时,graphics 实例完全有可能已经被处理掉。甚至有可能单元格不再是控件的一部分,或者控件本身不再存在。

同样重要的是,代码可能取决于您的代码更改内容 - 例如,在某些事件中,您在 EventArgs 中设置了一些值以指示例如成功,或者给一些 return 值。同样,这意味着您不能在内部使用 await - 据调用者所知,该函数只是 return 在您执行 await 的那一刻(除非它同步完成)。