Обновление GUI из асинхронного метода

При создании простого образца используется async/awaitЯ обнаружил, что некоторые примеры просто иллюстрируют закономерность Button1_Click как методы и свободно обновлять элементы управления GUI прямо из async методы. Так что можно считать это безопасным механизмом. Но мой тестовый код постоянно зависал на TargetInvocationException исключения в mscorlib.dll с внутренними исключениями, такими как: NullReference, ArgumentOutOfRange и т.д. Что касается трассировки стека, все, казалось, указывало на WinForms.StatusStrip метки, отображающие результаты (и приводимые непосредственно из async методы, связанные с обработчиками событий кнопки). Сбой, кажется, исправлен при использовании старой школы Control.Invoke при доступе к элементам управления GUI.

Вопросы: я пропустил что-то важное? Используются ли асинхронные методы так же, как потоки / фоновые рабочие, ранее использовавшиеся для долгосрочных операций, и, таким образом, Invoke такое рекомендуемое решение? Являются ли фрагменты кода вождением GUI непосредственно из async методы неправильные?

пример

РЕДАКТИРОВАТЬ: Для 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();
                }
            });
        }
    }
}

2 ответа

Решение

Ни Task ни await дать вам какие-либо гарантии в этом отношении. Необходимо учитывать контекст, в котором была создана задача, и где было опубликовано продолжение.

Если вы используете await в обработчике событий winforms контекст синхронизации перехватывается, и продолжение возвращается обратно в поток пользовательского интерфейса (на самом деле, это в значительной степени вызывает Invoke на данный блок кода). Однако, если вы просто начнете новую задачу с Task.Run, или ты await из другого контекста синхронизации это больше не применяется. Решение состоит в том, чтобы запустить продолжение в правильном планировщике задач, который вы можете получить из контекста синхронизации winforms.

Однако следует отметить, что это еще не обязательно означает, что async события будут работать правильно. Например, Winforms также использует события для таких вещей, как CellPaintingгде это на самом деле зависит от их синхронной работы. Если вы используете await в таком случае гарантированно не будет работать должным образом - продолжение все равно будет опубликовано в потоке пользовательского интерфейса, но это не обязательно сделает его безопасным. Например, предположим, что элемент управления имеет такой код:

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

К тому времени, когда ваше продолжение запускается, вполне возможно, что graphics экземпляр уже был утилизирован. Возможно даже, что ячейка больше не является частью элемента управления или что сам элемент управления больше не существует.

Не менее важно то, что код может зависеть от того, что ваш код меняет - например, есть события, в которых вы устанавливаете какое-то значение в их EventArgs чтобы указать, например, успех, или дать некоторое возвращаемое значение. Опять же, это означает, что вы не можете использовать await внутри - насколько известно вызывающей стороне, функция только что возвратила момент, когда вы делаете await (если это не завершается синхронно).

Поскольку вы используете асинхронные методы, я предполагаю, что код, который вы пытаетесь выполнить, находится не в потоке пользовательского интерфейса.

Посмотрите здесь: ТАК Вопрос

Другие вопросы по тегам