Как запустить и взаимодействовать с асинхронной задачей из графического интерфейса WPF

У меня есть графический интерфейс WPF, где я хочу нажать кнопку, чтобы начать длинную задачу, не замораживая окно на время выполнения задачи. Во время выполнения задачи я хотел бы получить отчеты о прогрессе, и я хотел бы включить еще одну кнопку, которая остановит задачу в любое время, когда я выберу.

Я не могу определить правильный способ использования async/wait/task. Я не могу включить все, что я пробовал, но это то, что у меня есть на данный момент.

Класс окна WPF:

public partial class MainWindow : Window
{
    readonly otherClass _burnBabyBurn = new OtherClass();
    internal bool StopWorking = false;

    //A button method to start the long running method
    private async void Button_Click_3(object sender, RoutedEventArgs e)
    {   
        Task burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);

        await burnTheBaby;
    }

    //A button Method to interrupt and stop the long running method
    private void StopButton_Click(object sender, RoutedEventArgs e)
    {
        StopWorking = true;
    }

    //A method to allow the worker method to call back and update the gui
    internal void UpdateWindow(string message)
    {
        TextBox1.Text = message;
    }
}

И класс для рабочего метода:

class OtherClass
{
    internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
    {       
        var tcs = new TaskCompletionSource<int>();       

        //Start doing work
        gui.UpdateWindow("Work Started");        

        While(stillWorking)
        {
        //Mid procedure progress report
        gui.UpdateWindow("Bath water n% thrown out");        
        if (gui.StopTraining) return tcs.Task;
        }

        //Exit message
        gui.UpdateWindow("Done and Done");       
        return tcs.Task;        
    }
}

Это выполняется, но окно функции WPF по-прежнему блокируется после запуска рабочего метода.

Мне нужно знать, как упорядочить объявления async/await/task, чтобы разрешить

A) рабочий метод не блокирует окно gui
B) пусть рабочий метод обновит окно gui
C) разрешить окну gui прекратить прерывание и остановить рабочий метод

Любая помощь или указатели очень оценены.

Ответ 1

Короче говоря:

private async void ButtonClick(object sender, RoutedEventArgs e)
{
    // modify UI object in UI thread
    txt.Text = "started";

    // run a method in another thread
    await Task.Run(()=> HeavyMethod(txt));
    // <<method execution is finished here>>

    // modify UI object in UI thread
    txt.Text = "done";
}

// This is a thread-safe method. You can run it in any thread
internal void HeavyMethod(TextBox textBox)
{
    while (stillWorking)
    {
        textBox.Dispatcher.Invoke(() =>
        {
            // UI operation goes inside of Invoke
            textBox.Text += ".";
        });

        // CPU-bound or I/O-bound operation goes outside of Invoke
        System.Threading.Thread.Sleep(51);
    }
}
Result:
started....................done

Объяснение:

  1. Вы можете использовать только await в методе async.

  2. Вы можете await только объект awaitable (т.е. Task, Task<T> или ValueTask<T> и т.д.)

  3. Task.Run обычно ставит в очередь Task в пуле потоков (т.е. использует существующий поток из пула потоков или создает новый поток в пуле потоков для запуска задачи. Это все верно, если Асинхронная операция не является чистой операцией, иначе не будет никакого потока, просто чистая асинхронная операция, выполняемая ОС и драйверами устройств)

  4. Выполнение ожидает в await завершения задачи и возвращает ее результаты, не блокируя основной поток из-за магической способности ключевого слова async:

  5. Волшебство ключевого слова async заключается в том, что оно не создает другого потока. Это только позволяет компилятору отказаться от и вернуть контроль над этим методом.

Не путайте метод с ключевым словом async с методом, заключенным в Task; Task отвечает за потоки, async отвечает за магию

Так

Ваш основной поток вызывает метод async (MyButton_Click), как обычный метод, и до сих пор без потоков... Теперь вы можете запустить задачу внутри MyButton_Click следующим образом:

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    //queue a task to run on threadpool
    Task task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    //wait for it to end without blocking the main thread
    await task;
}

или просто

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
}

или если ExecuteLongProcedure имеет возвращаемое значение типа string

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    Task<string> task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    string returnValue = await task;
}

или просто

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    string returnValue = await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));

    //or in cases where you already have a "Task returning" method:
    //  var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}

Метод внутри задачи (или ExecuteLongProcedure) выполняется асинхронно и выглядит следующим образом:

//change the value for the following flag to terminate the loop
bool stillWorking = true;

//calling this method blocks the calling thread
//you must run a task for it
internal void ExecuteLongProcedure(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");
        //the following line will block main thread unless
        //ExecuteLongProcedure is called with await keyword
        System.Threading.Thread.Sleep(51);
    }

    gui.UpdateWindow("Done and Done");
} 

Типы возврата:

Предположим, у вас есть private async <ReturnType> Method() { ... }

Если <ReturnType> - void, вы можете только выстрелить и забыть, т.е. просто вызвать метод обычным образом: Method();, а затем продолжить свою жизнь. Вот как WPF обрабатывает ваше событие нажатия кнопки. Если вы попытаетесь написать await Method();, вы получите сообщение об ошибке компиляции, которое не может ждать void.

Если <ReturnType> равен Task, то await Method(); ничего не возвращает. Также вы можете выстрелить и забыть: Method();.

Если <ReturnType> равно Task<T>, то значение, возвращаемое await Method();, является значением типа T. Также вы можете выстрелить и забыть: Method();.

Итак, async тоже делает магию:

Когда вы return получаете значение из метода async, оно будет заключено в Task<T>. Итак, если вы ожидаете int от вашего метода async, он должен вернуть Task<int>:

private async Task<int> GetOneAsync()
{
    return 1; // return type is a simple int 
              // while the method signature indicates a Task<int>
}

Как мы получаем значение, заключенное в Task<>? await разворачивает возвращаемое значение для вас:

int number = await Task.Run(() => 1); // right hand of await is a Task<int>
                                      // while the left hand is an int

разверните его так:

Func<int> GetOneFunc = () => 1; // synchronous function returning a number
Task<int> GetOneTask = Task.Run(GetOneFunc); // a Task<int> is started
int number = await GetOneTask; // waiting AND unwrapping Task<int> into int

Весь код:

private async Task<int> GetNumberAsync()
{
    int number = await Task.Run(GetNumber); // unwrap from Task<int>
    return number; // wrap in Task<int>
}

Я НЕ МОГУ В ЭТОМ ПОДЧЕРКНИТЬ! НЕ ДЕЛАЙТЕ ЭТОГО:

private async Task<int> GetNumberAsync()
{
    int number = Task.Run(GetNumber).Result; // sync over async
    return number;
}

Все еще в замешательстве? прочитайте асинхронные типы возврата на MSDN.

Примечание:

await является асинхронным и отличается от task.Wait(), который является синхронным. Но они оба делают одно и то же, ожидая завершения задачи.

await является асинхронным и отличается от task.Result, который является синхронным. Но оба они делают то же самое, ожидая, пока задача завершится, развернув и вернув результаты.

Чтобы иметь упакованное значение, вы всегда можете использовать Task.FromResult(1) вместо создания нового потока с помощью Task.Run(() => 1).

Task.Run является более новой (.NetFX4.5) и более простой версией Task.Factory.StartNew

Блокировка:

Связанные с процессором или связанные с IO операции, такие как Sleep, будут блокировать основной поток, даже если они вызываются в методе с ключевым словом async. (опять же, не путайте метод async с методом внутри Task. Очевидно, что это не так, если сам асинхронный метод выполняется как задача: await MyAsyncMethod)

await запрещает задаче блокировать основной поток, поскольку компилятор откажется от управления методом async.

private async void Button_Click(object sender, RoutedEventArgs e)
{
        Thread.Sleep(1000);//blocks
        await Task.Run(() => Thread.Sleep(1000));//does not block
}

Asynchronousy is viral

Обычно, когда вы пишете асинхронный метод, вы делаете все вызывающие асинхронными до конца. Если даже один вызывающий абонент не является асинхронной операцией, вам будет лучше без асинхронности.

(это не означает, что вам нужно везде добавлять асинхронное ключевое слово!)

В WPF GUI вы начинаете с асинхронного обработчика событий до самого конца до момента, когда выполняются операции с привязкой к процессору или с вводом-выводом (задача), и до того момента, когда объекты WPF с графическим интерфейсом выполняются манипулировать (вызывать).

Соглашение об именах

Просто добавьте название метода к ключевому слову async с помощью Async.

Поскольку избегание методов async void является хорошей практикой (см. шаблоны ниже), можно сказать, что только Task возвращающие методы должны иметь постфикс с Async.

Тип возвращаемого значения асинхронного метода должен быть void, Task, Task<T>, тип, похожий на задачу, IAsyncEnumerable<T> или IAsyncEnumerator<T>

WPF GUI:

Если вам нужен асинхронный доступ к GUI (внутри метода ExecuteLongProcedure), вызовите любую операцию, которая включает в себя изменение любого не поточно-ориентированного объекта. Например, любой объект WPF GUI должен вызываться с использованием объекта Dispatcher, который связан с потоком GUI:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Однако, если задача запускается в результате изменения свойства обратного вызова property changed callback из ViewModel, нет необходимости использовать Dispatcher.Invoke, поскольку обратный вызов фактически выполняется из потока пользовательского интерфейса.

Accessing collections on non-UI Threads

WPF позволяет получать доступ к коллекциям данных и изменять их в потоках, отличных от того, который создал коллекцию. Это позволяет использовать фоновый поток для получения данных из внешнего источника, такого как база данных, и отображения данных в потоке пользовательского интерфейса. Используя другой поток для изменения коллекции, ваш пользовательский интерфейс остается реагирующим на взаимодействие с пользователем.

Изменения значений, инициированные INotifyPropertyChanged, автоматически отправляются обратно диспетчеру.

Как включить межпоточный доступ

Помните, что сам метод async выполняется в основном потоке. Так что это действительно так:

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    txt.Text = "starting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure1());
    txt.Text = "waiting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure2());
    txt.Text = "finished"; // UI Thread
}

Patterns:

Огонь и забудь:

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async void DoAsync() // returns void
{
    await Task.Run(Do);
}
void FireAndForget() // not blocks, not waits
{
    DoAsync();
}

Стреляй и наблюдай:

Методы возврата к задачам лучше, поскольку необработанные исключения вызывают TaskScheduler.UnobservedTaskException.

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async Task DoAsync() // returns Task
{
    await Task.Run(Do);
}
void FireAndWait() // not blocks, not waits
{
    Task.Run(DoAsync);
}

Синхронно запускать и ждать, тратя ресурсы потоков:

Это известно как Синхронизация по асинхронному режиму, это синхронная операция, но она использует более одного потока, что может вызвать голодание. Это происходит, когда вы звоните в Wait() или пытаетесь прочитать результаты непосредственно из task.Result до завершения задачи.

(ИЗБЕГАЙТЕ ЭТОГО РИСУНКА)

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async Task DoAsync() // returns Task
{
    await Task.Run(Do);
}
void FireAndWait() // blocks, waits and uses 2 more threads. Yikes!
{
    var task = Task.Run(DoAsync);
    task.Wait();
}

Это все к этому?

Нет. Намного больше нужно узнать о async, его контексте и его продолжении. Этот блог особенно рекомендуется.

Задача использует поток? Вы уверены?

Не обязательно. Прочитайте этот ответ, чтобы узнать больше об истинном лице async.

Стивен Клири отлично объяснил async-await. Он также объясняет в своем другом сообщении в блоге, когда нет темы.

Читать дальше

ValueTask и Task

MSDN объясняет Task

MSDN объясняет async

как к вызову-асинхронный-метод-из-синхронной-методы

async await - За кулисами

async await - FAQ

Убедитесь, что вы знаете разницу между асинхронным, параллельным и параллельным.

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

Исследовать одновременное пространство имен

В конце концов, прочитайте эту электронную книгу: Patterns_of_Parallel_Programming_CSharp

Ответ 2

Неверное использование TaskCompletionSource<T>. TaskCompletionSource<T> - это способ создания TAP-совместимых оберток для асинхронных операций. В вашем методе ExecuteLongProcedureAsync образец кода - все связанные с ЦП (т.е. Синхронно, асинхронно).

Итак, гораздо более естественно писать ExecuteLongProcedure как синхронный метод. Также рекомендуется использовать стандартные типы для стандартного поведения, в частности используя IProgress<T> для обновлений прогресса и CancellationToken для отмены:

internal void ExecuteLongProcedure(int param1, int param2, int param3,
    CancellationToken cancellationToken, IProgress<string> progress)
{       
  //Start doing work
  if (progress != null)
    progress.Report("Work Started");

  while (true)
  {
    //Mid procedure progress report
    if (progress != null)
      progress.Report("Bath water n% thrown out");
    cancellationToken.ThrowIfCancellationRequested();
  }

  //Exit message
  if (progress != null)
    progress.Report("Done and Done");
}

Теперь у вас есть более многоразовый тип (без зависимостей GUI), который использует соответствующие соглашения. Его можно использовать как таковой:

public partial class MainWindow : Window
{
  readonly otherClass _burnBabyBurn = new OtherClass();
  CancellationTokenSource _stopWorkingCts = new CancellationTokenSource();

  //A button method to start the long running method
  private async void Button_Click_3(object sender, RoutedEventArgs e)
  {
    var progress = new Progress<string>(data => UpdateWindow(data));
    try
    {
      await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3,
          _stopWorkingCts.Token, progress));
    }
    catch (OperationCanceledException)
    {
      // TODO: update the GUI to indicate the method was canceled.
    }
  }

  //A button Method to interrupt and stop the long running method
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
    _stopWorkingCts.Cancel();
  }

  //A method to allow the worker method to call back and update the gui
  void UpdateWindow(string message)
  {
    TextBox1.Text = message;
  }
}

Ответ 3

Это упрощенная версия самого популярного ответа Биджана. Я упростил биджанский ответ, чтобы помочь мне разобраться в проблеме, используя удобное форматирование, предоставляемое Stack Overflow.

Внимательно прочитав и отредактировав сообщение Биджана, я наконец понял: как ждать завершения асинхронного метода?

В моем случае выбранный ответ для этого другого поста в конечном итоге привел меня к решению моей проблемы:

"Избегайте async void. Пусть ваши методы возвращают Task вместо void. Тогда вы можете их await ".

Вот моя упрощенная версия ответа Биджана (отлично):

1) Это запускает задачу, используя async и ожидает:

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    // if ExecuteLongProcedureAsync has a return value
    var returnValue = await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}

2) Это метод для асинхронного выполнения:

bool stillWorking = true;
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");

        //the following line blocks main thread unless
        //ExecuteLongProcedureAsync is called with await keyword
        System.Threading.Thread.Sleep(50);
    }

    gui.UpdateWindow("Done and Done");
} 

3) вызвать операцию, которая включает в себя свойство из GUI:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Или же,

void UpdateWindow(string text)
{
    //simply
    txt.Text += text;
}

Заключительные комментарии) В большинстве случаев у вас есть два метода.

  • Первый метод (Button_Click_3) вызывает второй метод и имеет модификатор async, который указывает компилятору включить многопоточность для этого метода.

    • Thread.Sleep в async методе блокирует основной поток. но ждать задачи нет.
    • Выполнение останавливается в текущем потоке (втором потоке) в операторах await до тех пор, пока задача не будет завершена.
    • Вы не можете использовать await вне async метода
  • Второй метод (ExecuteLongProcedureAsync) ExecuteLongProcedureAsync в задачу и возвращает общий объект Task<original return type> который можно поручить обрабатывать асинхронно, добавив перед ним await.

    • Все в этом методе выполняется асинхронно

Важный:

Лиеро поднял важный вопрос. Когда вы связываете элемент со свойством ViewModel, свойство измененного обратного вызова выполняется в потоке пользовательского интерфейса. Поэтому нет необходимости использовать Dispatcher.Invoke. Изменения значений, инициированные INotifyPropertyChanged, автоматически направляются обратно в диспетчер.

Ответ 4

Вот пример использования async/await, IProgress<T> и CancellationTokenSource. Это современные функции языка С# и .Net Framework, которые вы должны использовать. Другие решения делают мои глаза немного кровоточат.

Функции кода

  • Подсчитайте до 100 в течение 10 секунд.
  • Отображение прогресса на индикаторе выполнения
  • Длительная работа (период ожидания), выполненный без блокировки пользовательского интерфейса
  • Отключение пользователя с задержкой
  • Инкрементные обновления прогресса
  • Отчет о состоянии работы после

Вид

<Window x:Class="ProgressExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
    <StackPanel>
        <Button x:Name="Button_Start" Click="Button_Click">Start</Button>
        <ProgressBar x:Name="ProgressBar_Progress" Height="20"  Maximum="100"/>
        <Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
    </StackPanel>
</Window>

Код

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private CancellationTokenSource currentCancellationSource;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            // Enable/disabled buttons so that only one counting task runs at a time.
            this.Button_Start.IsEnabled = false;
            this.Button_Cancel.IsEnabled = true;

            try
            {
                // Set up the progress event handler - this instance automatically invokes to the UI for UI updates
                // this.ProgressBar_Progress is the progress bar control
                IProgress<int> progress = new Progress<int>(count => this.ProgressBar_Progress.Value = count);

                currentCancellationSource = new CancellationTokenSource();
                await CountToOneHundredAsync(progress, this.currentCancellationSource.Token);

                // Operation was successful. Let the user know!
                MessageBox.Show("Done counting!");
            }
            catch (OperationCanceledException)
            {
                // Operation was cancelled. Let the user know!
                MessageBox.Show("Operation cancelled.");
            }
            finally
            {
                // Reset controls in a finally block so that they ALWAYS go 
                // back to the correct state once the counting ends, 
                // regardless of any exceptions
                this.Button_Start.IsEnabled = true;
                this.Button_Cancel.IsEnabled = false;
                this.ProgressBar_Progress.Value = 0;

                // Dispose of the cancellation source as it is no longer needed
                this.currentCancellationSource.Dispose();
                this.currentCancellationSource = null;
            }
        }

        private async Task CountToOneHundredAsync(IProgress<int> progress, CancellationToken cancellationToken)
        {
            for (int i = 1; i <= 100; i++)
            {
                // This is where the 'work' is performed. 
                // Feel free to swap out Task.Delay for your own Task-returning code! 
                // You can even await many tasks here

                // ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting
                // This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
                await Task.Delay(100, cancellationToken).ConfigureAwait(false);

                // If cancelled, an exception will be thrown by the call the task.Delay
                // and will bubble up to the calling method because we used await!

                // Report progress with the current number
                progress.Report(i);
            }
        }

        private void Button_Cancel_Click(object sender, RoutedEventArgs e)
        {
            // Cancel the cancellation token
            this.currentCancellationSource.Cancel();
        }
    }