简体   繁体   中英

Updating GUI from async method

During creating simple sample using async/await , I discovered, that some examples just illustrate the pattern on Button1_Click like methods and freely update GUI controls directly from async methods. So one could consider this as the safe mechanism. But my test code was constantly crashing on TargetInvocationException exceptions in mscorlib.dll with inner exceptions like: NullReference , ArgumentOutOfRange etc. Regarding the stack trace, everything seemed to point to the WinForms.StatusStrip labels displaying results (and being driven directly from the async methods bound to the button event handlers). The crashing seems to be fixed when using the old school Control.Invoke when accessing the GUI controls.

The questions are: Have I missed something important? Are the async methods usafe the same way as the threads/background workers formerly used for long term operations and thus the Invoke is the recommended solution? Are the code snippets driving GUI directly from async methods wrong?

example

EDIT: For the missing-source downvoters: Create a Simple Form containing three buttons and one StatusStrip containing two labels...

//#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();
                }
            });
        }
    }
}

Neither Task nor await give you any guarantees in this respect. You need to consider the context where the task was created, and where the continuation was posted.

If you're using await in a winforms event handler, the synchronization context is captured, and the continuation returns back to the UI thread (in fact, it pretty much calls Invoke on the given block of code). However, if you just start a new task with Task.Run , or you await from another synchronization context, this no longer applies. The solution is to run the continuation on the proper task scheduler you can get from the winforms synchronization context.

However, it should be noted that it still doesn't necessarily mean that async events will work properly. For example, Winforms also uses events for things like CellPainting , where it actually depends on them running synchronously. If you use await in such an event, it's pretty much guaranteed not to work properly - the continuation will still be posted to the UI thread, but that doesn't necessarily make it safe. For example, suppose that the control has code like this:

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

By the time your continuation runs, it's entirely possible the graphics instance has already been disposed of. It's even possible the cell is no longer part of the control, or that the control itself no longer exists.

Just as importantly, the code might depend on your code changing things - for example, there's events where you set some value in their EventArgs to indicate eg success, or to give some return value. Again, this means you can't use await inside - as far as the caller is aware, the function just returned the moment you do the await (unless it completes synchronously).

Since you're using an async Methods, My guess is that the code you're trying to execute is not on the UI Thread.

Take a look here: SO Question

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM