从异步方法更新GUI

sha*_*ner 1 c# asynchronous winforms thread-synchronization

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

问题是:我错过了重要的事情吗?异步方法是否与先前用于长期操作的线程/后台工作程序相同,因此Invoke是推荐的解决方案?直接从async方法错误驱动GUI的代码片段是什么?

编辑:对于缺失源的downvoters:创建一个包含三个按钮的简单表单和一个包含两个标签的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();
                }
            });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Lua*_*aan 6

在这方面也Task没有await给你任何保证.您需要考虑创建任务的上下文以及发布延续的位置.

如果您await在winforms事件处理程序中使用,则捕获同步上下文,并且continuation返回到UI线程(实际上,它几乎调用Invoke给定的代码块).但是,如果你刚开始一个新的任务Task.Run,或者你await从其他的同步环境,这已不再适用.解决方案是在winforms同步上下文中运行适当的任务调度程序上运行continuation.

但是,应该指出,它仍然不一定意味着async事件将正常工作.例如,Winforms还将事件用于诸如CellPainting实际依赖于它们同步运行的事物.如果你await在这样的事件中使用,它几乎可以保证不能正常工作 - 延续仍将发布到UI线程,但这并不一定使它安全.例如,假设控件具有如下代码:

using (var graphics = NewGraphics())
{
  foreach (var cell in cells)
    CellPainting(cell, graphics);
}
Run Code Online (Sandbox Code Playgroud)

当你的继续运行时,graphics实例已经完全被处理掉了.甚至可能细胞不再是控制的一部分,或者控制本身不再存在.

同样重要的是,代码可能取决于您的代码更改事物 - 例如,您可以在其中设置一些值EventArgs以指示例如成功或提供一些返回值的事件.同样,这意味着你不能await在内部使用- 只要调用者知道,函数只是在你执行时返回await(除非它同步完成).