How to run and interact with an async Task from a WPF gui

前端 未结 4 1209
隐瞒了意图╮
隐瞒了意图╮ 2020-11-28 01:33

I have a WPF GUI, where I want to press a button to start a long task without freezing the window for the duration of the task. While the task is running I would like to get

相关标签:
4条回答
  • 2020-11-28 01:37

    This is a simplified version of the most popular answer here by Bijan. I simplified Bijan's answer to help me think through the problem using the nice formatting provided by Stack Overflow.

    By carefully reading and editing Bijan's post I finally understood: How to wait for async method to complete?

    In my case the chosen answer for that other post is what ultimately led me to solve my problem:

    "Avoid async void. Have your methods return Task instead of void. Then you can await them."

    My simplified version of Bijan's (excellent) answer follows:

    1) This starts a task using async and await:

    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) This is the method to execute asynchronously:

    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) Invoke the operation which involves a property from gui:

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

    Or,

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

    Closing comments) In most cases you have two methods.

    • First method (Button_Click_3) calls the second method and has the async modifier which tells the compiler to enable threading for that method.

      • Thread.Sleep in an async method blocks the main thread. but awaiting a task does not.
      • Execution stops on current thread (second thread) on await statements until task is finished.
      • You can't use await outside an async method
    • Second method (ExecuteLongProcedureAsync) is wrapped within a task and returns a generic Task<original return type> object which can be instructed to be processed asynchronously by adding await before it.

      • Everything in this method in executed asynchronously

    Important:

    Liero brought up an important issue. When you are Binding an element to a ViewModel property, the property changed callback is executed in UI thread. So there is no need to use Dispatcher.Invoke. Value changes fired by INotifyPropertyChanged are automatically marshalled back onto the dispatcher.

    0 讨论(0)
  • 2020-11-28 01:51

    Your use of TaskCompletionSource<T> is incorrect. TaskCompletionSource<T> is a way to create TAP-compatible wrappers for asynchronous operations. In your ExecuteLongProcedureAsync method, the sample code is all CPU-bound (i.e., inherently synchronous, not asynchronous).

    So, it's much more natural to write ExecuteLongProcedure as a synchronous method. It's also a good idea to use standard types for standard behaviors, in particular using IProgress<T> for progress updates and CancellationToken for cancellation:

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

    Now you have a more reusable type (no GUI dependencies) that uses the appropriate conventions. It can be used as such:

    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;
      }
    }
    
    0 讨论(0)
  • 2020-11-28 01:52

    Here is an example using async/await, IProgress<T> and CancellationTokenSource. These are the modern C# and .Net Framework language features that you should be using. The other solutions are making my eyes bleed a bit.

    Code Features

    • Count to 100 over a period of 10 seconds
    • Display progress on a progress bar
    • Long running work (a 'wait' period) performed without blocking the UI
    • User triggered cancellation
    • Incremental progress updates
    • Post operation status report

    The view

    <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>
    

    The code

        /// <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();
            }
        }
    
    0 讨论(0)
  • 2020-11-28 02:02

    Long story short:

    private async void ButtonClick(object sender, RoutedEventArgs e)
    {
        // modify UI object in UI thread
        txt.Text = "started";
    
        // run a method in another thread
        await 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 async Task HeavyMethod(TextBox textBox)
    {
        while (stillWorking)
        {
            textBox.Dispatcher.Invoke(() =>
            {
                // UI operation goes inside of Invoke
                textBox.Text += ".";
                // Note that: 
                //    Dispatcher.Invoke() blocks the UI thread anyway
                //    but without it you can't modify UI objects from another thread
            });
            
            // CPU-bound or I/O-bound operation goes outside of Invoke
            // await won't block UI thread, unless it's run in a synchronous context
            await Task.Delay(51);
        }
    }
    
    Result:
    started....................done
    

    You need to know about (1) how to write async code and (2) how to run UI operations in another thread.

    The magic of async and await:

    1. You can only await in an async method.

    2. You can only await an awaitable object (i.e. Task, Task<T> or ValueTask<T> etc)

    3. The return type of an async method is wrapped in a Task and await unwraps it. (see Wrapping and Unwrapping section)

    4. Task.Run usually queues a Task in the thread pool

    (i.e. it uses an existing thread or creates a new thread in the thread pool to run the task. Unless it's a pure operation)

    1. The execution waits at await for the task to finish and returns back its results, without blocking the main thread because of the magic:

    2. The magic of async-await is that it uses a state-machine to let the compiler give up and take back the control over the awaited Task in an async method.

      (i.e. async method does not run in another thread. async and await by themselves don't have anything to do with thread creation.)

    Don't confuse the method with async keyword with the method wrapped within a Task; The Task is responsible for threading, the async is responsible for the magic

    So

    By putting async in the method signature you tell the compiler to use state machine to call this method (no threading so far). Then by running a Task you use a thread to call the method inside the task. And by awaiting the task you prevent the execution flow to move past the await line without blocking UI thread.

    The event handler looks like the code below.

    Two possible cases for presense of async in the signature of ExecuteLongProcedure (case 1 and 2) and MyButton_Click (case A and B) are explained:

    private async void MyButton_Click(object sender, RoutedEventArgs e)
    {
        //queue a task to run on threadpool
    
        // 1. if ExecuteLongProcedure is a normal method and returns void
        Task task = Task.Run(()=>
            ExecuteLongProcedure(this, intParam1, intParam2, intParam3)
        );
        // or
        // 2. if ExecuteLongProcedure is an async method and returns Task
        Task task = ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
    
        // either way ExecuteLongProcedure is running asynchronously here
        // the method will exit if you don't wait for the Task to finish
    
        // A. wait without blocking the main thread
        //  -> requires MyButton_Click to be async
        await task;
        // or
        // B. wait and block the thread (NOT RECOMMENDED AT ALL)
        // -> does not require MyButton_Click to be async
        task.Wait();
    }
    

    async method return types:

    This is very important to know. Suppose you have the following declaration:

    private async ReturnType Method() { ... }
    
    • If ReturnType is void you can't await it

      • If you try writing await Method();, you will get a compile error saying cannot await void.
      • You can only fire and forget i.e. just call the method normally: Method(); and then go on with your life.
      • The Method execution will be synchronous, however since it has async it will allow you to take advantage of the magic, i.e. you can write await task within the method to control the flow of execution.
      • This is how WPF handles your button click event handler, obviously because your event handler returns void.
    • If ReturnType is Task then await Method(); returns void

    • If ReturnType is Task<T> then await Method(); returns a value of type T

    The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

    You might want to scroll down to WPF GUI section if you don't want to learn more!

    Wrapping and Unrwapping:

    Wrapping:

    async methods wrap their return values in a Task.

    E.g., this method wraps a Task around an int and returns it:

    //      async Task<int>
    private async Task<int> GetOneAsync()
    {
        int val = await CalculateStuff();
        return val;
    //  returns an integer
    }
    

    Unwrapping:

    To retrieve or unwrap the value which is wrapped inside a Task<>:

    • asynchronous option: await
    • synchronous option: task.Result or task.GetAwaiter().GetResult() or task.WaitAndUnwrapException() or read How to call asynchronous method from synchronous method in C#?

    e.g. await unwraps the int out of the Task:

    Task<int> task = GetOneAsync();
    int number = await task;
    //int     <-       Task<int>
    

    Different ways to wrap and unwrap:

    private Task<int> GetNumber()
    {
        Task<int> task;
    
        task = Task.FromResult(1); // the correct way to wrap a quasi-atomic operation, the method GetNumber is not async
        task = Task.Run(() => 1); // not the best way to wrap a number
    
        return task;
    }
    
    private async Task<int> GetNumberAsync()
    {
        int number = await Task.Run(GetNumber); // unwrap int from Task<int>
    
        // bad practices:
        // int number = Task.Run(GetNumber).GetAwaiter().GetResult(); // sync over async
        // int number = Task.Run(GetNumber).Result; // sync over async
        // int number = Task.Run(GetNumber).Wait(); // sync over async
    
        return number; // wrap int in Task<int>
    }
    

    Still confused? Read async return types on MSDN.

    To unwrap a task result, Always try to use await instead of .Result otherwise there will be no asynchronous benefit but only asynchronous disadvantages. The latter is called "sync over async".

    Note:

    await is a asynchronous and is different from task.Wait() which is synchronous. But they both do the same thing which is waiting for the task to finish.

    await is a asynchronous and is different from task.Result which is synchronous. But they both do the same thing which is waiting for the task to finish and unwrapping and returning back the results.

    To have a wrapped value, you can always use Task.FromResult(1) instead of creating a new thread by using Task.Run(() => 1).

    Task.Run is newer (.NetFX4.5) and simpler version of Task.Factory.StartNew

    Naming Convention

    Simply postfix the name of the method with the async keyword with Async.

    Since avoiding async void methods is a good practice (see patterns below), you can say only Task returning methods should be postfixed with Async.

    The purpose of this convention is to make sure the Asynchronous Virality is respected.


    WPF GUI:

    This is where I explain how to run UI operations in another thread.


    Blocking:

    First thing you need to know about WPF async event handlers is that the Dispatcher will provide a synchronization context. Explained here

    CPU-bound or IO-bound operations such as Sleep and task.Wait() will block and consume the thread even if they are called in a method with async keyword. but await Task.Delay() tells the state-machine to stop the flow of execution on the thread so it does not consume it; meaning that the thread resources can be used elsewhere:

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
            Thread.Sleep(1000);//stops, blocks and consumes threadpool resources
            await Task.Delay(1000);//stops without consuming threadpool resources
            Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources
            await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do
    }
    

    Thread Safety:

    If you have to access GUI asynchronously (inside ExecuteLongProcedure method), invoke any operation which involves modification to any non-thread-safe object. For instance, any WPF GUI object must be invoked using a Dispatcher object which is associated with the GUI thread:

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

    However, If a task is started as a result of a property changed callback from the ViewModel, there is no need to use Dispatcher.Invoke because the callback is actually executed from the UI thread.

    Accessing collections on non-UI Threads

    WPF enables you to access and modify data collections on threads other than the one that created the collection. This enables you to use a background thread to receive data from an external source, such as a database, and display the data on the UI thread. By using another thread to modify the collection, your user interface remains responsive to user interaction.

    Value changes fired by INotifyPropertyChanged are automatically marshalled back onto the dispatcher.

    How to enable cross-thread access

    Remember, async method itself runs on the main thread. So this is valid:

    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
    }
    

    Another way to invoke UI operations from UI thread is to use SynchronizationContext as described here. SynchronizationContext is a stronger abstraction than Dispatcher and it's cross-platform.

    var uiContext = SynchronizationContext.Current;
    while (stillWorking)
    {
        uiContext.Post(o =>
        {
            textBox.Text += ".";
        }, null);
        await Task.Delay(51);
    }
    

    Patterns:

    Fire and forget pattern:

    For obvious reasons this is how your WPF GUI event handlers such as Button_Click are called.

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

    Fire and observe:

    Task-returning methods are better since unhandled exceptions trigger the 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);
    }
    

    Fire and wait synchronously while wasting thread resources:

    This is known as Sync over async, it is a synchronous operation but it uses more than one thread which may cause starvation. This happens when you call Wait() or try to read results directly from task.Result before the task is finished.

    (AVOID THIS PATTERN)

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

    Is this all to it?

    No. There is a lot more to learn about async, its context and its continuation. This blogpost is especially recommended.

    Task uses Thread? Are you sure?

    Not necessarily. Read this answer to know more about the true face of async.

    Stephen Cleary has explained async-await perfectly. He also explains in his other blog post when there is no thread involved.

    Read more

    ValueTask and Task

    MSDN explains Task

    MSDN explains async

    how-to-call-asynchronous-method-from-synchronous-method

    async await - Behind the scenes

    async await - FAQ

    Make sure you know the difference between Asynchronous, Parallel and Concurrent.

    You may also read a simple asynchronous file writer to know where you should concurrent.

    Investigate concurrent namespace

    Ultimately, read this e-book: Patterns_of_Parallel_Programming_CSharp

    0 讨论(0)
提交回复
热议问题