【问题标题】:async/await debugging in WinForms: system.invalidoperationexception cross-thread operation not validWinForms 中的 async/await 调试:system.invalidoperationexception 跨线程操作无效
【发布时间】:2017-07-31 21:10:07
【问题描述】:

在 Visual Studio 2017 中通过 Ctrl+F5(不带调试启动)启动/运行您的应用程序并使用 async/await 处理 winforms 的控件事件时。例如,按钮单击事件处理,您可以访问这些控件属性进行读/写操作,但是当您按 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) ...

代码构造。

问题:是否有任何/更优雅的方式来避免使用 .InvokeRequired 没有以下代码示例片段中的条件编译:

#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 代码构造是否是最佳/推荐的解决方案?

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

【问题讨论】:

    标签: winforms debugging async-await runtime-error cross-thread


    【解决方案1】:

    有关进度更新,use IProgress&lt;T&gt;/Progress&lt;T&gt;

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

    【讨论】:

    • 谢谢,但问题不在于(大量)进度更新 - 它是关于在异步/等待事件处理代码(周期)后面的调试/跟踪模式 (F5) 中的任何控件更新我可以在哪里写很多对拥有表单控件的引用。在我看来,将所有写入引用放入 IProgress<...> 块并不会使代码看起来更好 - 毕竟在事件后面的异步/等待形式中没有任何必要,也不需要 .InvokeRequired/.Invoke 或 IProgress 在运行时模式(Ctrl+F5)或生产环境中执行代码时?
    • 你是否在调试器中运行并不重要; IProgress&lt;T&gt; 是进度更新的合适选择。 Progress&lt;T&gt; 实现管理 UI 上下文方面,因此不需要 Invoke
    • 谢谢。我仍然不清楚这个主题。假设我有一个 WinForms DataGridView 和一个按钮 (cmdUpdate),单击时开始一个长时间运行的循环,以使用在此循环中收集的值更新 DataGridView 行的单元格中选择的值。 cmdTest_Click 方法具有 async 修饰符。如果每个选定行的新值与其当前视图不同,则必须更新每个选定行的五个单元格。更新周期的代码在 cmdUpdate_Click 方法中实现为 await Task.Run(() => ....(待续)
    • (续) 问题:更新单元格值并避免“system.invalidoperationexception cross-thead operation not valid...”在更新单元格时出现运行时错误的适当方法是 - 使用 . InvokeRequired /.Invoke 或 ... ?注意:更新五个单元格中的每一个的代码行不是必然的 - 每个单元格值更新代码行都有一些更新值计算/准备代码行。
    • 我很难关注你的 cmets。如果您需要使用 results 更新 UI,那么 myCellValue = await Task.Run(..); 之类的就可以了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-11
    • 1970-01-01
    相关资源
    最近更新 更多