async/await 在 WinForms 中调试:system.invalidoperationexception 跨线程操作无效

async/await debugging in WinForms: system.invalidoperationexception cross-thread operation not valid

当 starting/running 您的应用程序在 Visual Studio 2017 中通过 Ctrl+F5(不调试启动)并使用 async/await 进行 winforms 的控件事件处理。例如,按钮单击事件处理,您可以访问这些控件属性以进行 read/write 操作,但是当您通过 F5 启动应用程序时,您会收到运行时错误消息:

system.invalidoperationexception cross-thread operation not valid: 
Control '{{controlName}}' accessed from a thread other than the thread it was created on.'

要解决这个问题你必须使用众所周知的

if (this.InvokeRequired) ...

代码构建。

问题:是否有any/more优雅的方法来避免使用 .InvokeRequired without conditional compilation presented in the following code sample片段:

#define DEBUG_TRACE

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

namespace WindowsFormsAppToTestAsyncAwait
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }
        private
            async
            void cmdTest_Click(object sender, EventArgs e)
        {
            await Task.Run(() =>
            {
#if DEBUG_TRACE
                inv(()=>
#endif
                txtTest.Text = ""
#if DEBUG_TRACE
                )
#endif
                ;
                for (long i = 1; i < 100000000; i++)
                {
                    if (i % 10000000 == 1)
#if DEBUG_TRACE
                        inv(() =>
#endif
                        txtTest.Text =
                            txtTest.Text +
                                i.ToString() + System.Environment.NewLine
#if DEBUG_TRACE
                                )
#endif
                                ;
                }

            });
        }

 #if DEBUG_TRACE
        private void inv(Action a)
        {
            if (this.InvokeRequired) this.Invoke (a); else a();
        }
 #endif
 }
}

更新

问题:在下面的代码示例中 statsProgress 代码构造是否是最 optimal/recommended 的解决方案?

private async void cmdTest_Click(object sender, EventArgs e)
{
    double runningSum = 0;
    long totalCount = 0;
    double average = 0;

    IProgress<long> progress = new Progress<long>(i =>
    {
        txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
    });

    IProgress<object> statsProgress = new Progress<object>(o =>
    {
        txtRunningSum.Text = runningSum.ToString();
        txtTotalCount.Text = totalCount.ToString();
        txtAverage.Text = average.ToString();
    });

    txtTest.Text = "";

    await Task.Run(() =>
    {
        for (long i = 1; i < 100000000; i++)
        {
            runningSum += i;
            totalCount += 1;
            average = runningSum / totalCount;

            if (i % 10000000 == 1)  progress?.Report(i);
            // in general case there could be many updates of controls' values
            // from within this awaited Task with every update issued/fired
            // on different steps of this long running cycle
            if (i % (10000000 / 2) == 1) statsProgress?.Report(default(object));
        }
    });
}

更新 2

这是最终的解决方案:

internal struct UpdateStats
{
    internal double RunningSum;
    internal long TotalCount;
    internal double Average;
}
private async void cmdTest_Click(object sender, EventArgs e)
{
    UpdateStats stats = new UpdateStats();
    IProgress<long> progress = new Progress<long>(i =>
    {
        txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
    });

    IProgress<UpdateStats> statsProgress = new Progress<UpdateStats>(o =>
    {
        txtRunningSum.Text = o.RunningSum.ToString();
        txtTotalCount.Text = o.TotalCount.ToString();
        txtAverage.Text = o.Average.ToString();
    });

    txtTest.Text = "";

    await Task.Run(() =>
    {
        const int MAX_CYCLE_COUNT = 100000000;
        for (long i = 1; i <= MAX_CYCLE_COUNT; i++)
        {
            stats.RunningSum += i;
            stats.TotalCount += 1;
            stats.Average = stats.RunningSum / stats.TotalCount;

            if (i % 10000000 == 1) progress?.Report(i);
            // in general case there could be many updates of controls' values
            // from within this awaited Task with every update issued/fired
            // on different steps of this long running cycle
            if (i % (10000000 / 2) == 1) statsProgress?.Report(stats);
        }

        progress?.Report(MAX_CYCLE_COUNT);
        statsProgress?.Report(stats);
    });
}

进度更新,use IProgress<T>/Progress<T>

private async void cmdTest_Click(object sender, EventArgs e)
{
  IProgress<int> progress = new Progress<int>(i =>
  {
      txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
  });

  txtTest.Text = "";
  await Task.Run(() =>
  {
    for (long i = 1; i < 100000000; i++)
    {
      if (i % 10000000 == 1)
        progress?.Report(i);
    }
  });
}