Monday, September 11, 2023

Async and Await

I have never taken the time to understand Async and Await modifiers. There are a few places in the application I'm currently working on where the application becomes non-responsive for a few seconds and I'd like to see if I can fix that. Async and Await are obvious candidates.

I wrote a test program that has four features.

  1. Calling a method synchronously
  2. Calling a method asynchronously.
  3. Calling a synchronous method from an asynchronous method
  4. Calling an asynchronous method from a synchronous method.

Start a new C#, .Net Core project called AsyncAwait. Change the MainWindow xaml and code to look like this.

<Window x:Class="AsyncAwait.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"
        xmlns:local="clr-namespace:AsyncAwait"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="SyncCommand"/>
        <RoutedCommand x:Key="AsyncCommand"/>
        <RoutedCommand x:Key="SyncToAsyncCommand"/>
        <RoutedCommand x:Key="AsyncToSyncCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource SyncCommand}" Executed="SyncExecuted"/>
        <CommandBinding Command="{StaticResource AsyncCommand}" Executed="AsyncExecuted"/>
        <CommandBinding Command="{StaticResource SyncToAsyncCommand}" Executed="SyncToAsyncExecuted"/>
        <CommandBinding Command="{StaticResource AsyncToSyncCommand}" Executed="AsyncToSyncExecuted"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
        <Button Grid.Row="0" Content="Sync" Command="{StaticResource SyncCommand}"/>
        <Button Grid.Row="1" Content="ASync" Command="{StaticResource AsyncCommand}"/>
        <Button Grid.Row="2" Content="Sync to Async" Command="{StaticResource SyncToAsyncCommand}"/>
        <Button Grid.Row="3" Content="Async to Sync" Command="{StaticResource AsyncToSyncCommand}"/>
        <ListView Grid.Row="4" ItemsSource="{Binding Items}"/>
    </Grid>
</Window>

 ---------------------------------------------------------------------

using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
 
namespace AsyncAwait
{
    [ObservableObject]
    public partial class MainWindow : Window
    {
        [ObservableProperty]
        private ObservableCollection<string> items = new ObservableCollection<string>();
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        private void InitItems()
        {
            Items.Clear();
            AppendItem("InitItems");
        }
 
        private async Task InitItemsAsync()
        {
            await Application.Current.Dispatcher.BeginInvoke(() => Items.Clear());
            await AppendItemAsync("InitItemsAsync");
        }
 
        private void AppendItem(string Item)
        {
            string s = $"{System.DateTime.Now.ToString("HH:mm:ss.fff")} {Item}";
            Items.Add(s);
            Debug.WriteLine(s);
        }
 
        private async Task AppendItemAsync(string Item)
        {
            string s = $"{System.DateTime.Now.ToString("HH:mm:ss.fff")} {Item}";
            await Application.Current.Dispatcher.BeginInvoke(() => Items.Add(s));
            Debug.WriteLine(s);
        }
 
        private void SyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            InitItems();
            AppendItem("SyncExecuted");
            SyncExecuted2();
            AppendItem("SyncExecuted Exit");
        }
 
        private void SyncExecuted2()
        {
            AppendItem("SyncExecuted2");
            AppendItem("Pause 2 seconds");
            Thread.Sleep(2000);
            AppendItem("SyncExecuted2 Exit");
        }
 
        private async void AsyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            await InitItemsAsync();
            await AppendItemAsync("AsyncExecuted");
            await AsyncExecuted2();
            await AppendItemAsync("AsyncExecuted Exit");
        }
 
        private async Task AsyncExecuted2()
        {
            await AppendItemAsync("AsyncExecuted2");
            await AppendItemAsync("Pause 2 seconds");
            await Task.Delay(2000);
            await AppendItemAsync("AsyncExecuted2 Exit");
        }
 
        private void SyncToAsyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            InitItems();
            AppendItem("SyncToAsyncExecuted");
            SyncToAsyncExecuted2();
            AppendItem("SyncToAsyncExecuted Exit");
        }
 
        private async void SyncToAsyncExecuted2()
        {
            await AppendItemAsync("SyncToAsyncExecuted2");
            await AppendItemAsync("Pause 2 seconds");
            await Task.Delay(2000);
            await AppendItemAsync("SyncToAsyncExecuted2 Exit");
        }
 
        private async void AsyncToSyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            await InitItemsAsync();
            await AppendItemAsync("AsyncExecuted");
            AsyncToSyncExecuted2();
            await AppendItemAsync("AsyncExecuted Exit");
        }
 
        private void AsyncToSyncExecuted2()
        {
            AppendItem("AsyncToSyncExecuted2");
            AppendItem("Pause 2 seconds");
            Thread.Sleep(2000);
            AppendItem("AsyncToSyncExecuted2 Exit");
        }
    }
}

Run the program and click the [Sync] button. Try moving the window while the code is running.


You will notice you cannot move the window while the method is executing and the listbox is only populated once the SyncExecuted method has finished. This is the behavior we are trying to avoid.

Now click the [Async] button and try to move the window.


You can move the window while the AsyncExecuted method is executing and the results are shown as the code executes. The is the behavior we want.

The first thing to notice is the AsyncExecuted event handler is marked async void which is how you call an async method from a non-async method. AsyncExecuted calls async versions of everything. This means they are running on non-UI threads. The Items collection is owned by the UI thread which means we have to use Dispatcher.BeginInvoke to update the collection. If we don't use BeginInvoke we will get a run-time exception.

So it looks like it's fairly easy to add some Async and Await operators and solve our responsiveness problem. But there are some wrinkles...

One of the problems with Async and Await is that you can only call Await from an Async method and you can only call an Async method with the Await modifier. This forces us to modify our entire call stack if we want one method in it to run asynchronously. We need to figure out how to call an async method from a non-async method and vice versa.

The third button calls a non-async event handler called SyncToAsyncExecuted and this method calls the async method SyncToAsyncExecuted2. If you click the third button, take a close look at the output.


Notice that the SyncToAsyncExecuted method exits immediately. It did not wait for the SyncToAsyncExecuted2 method to complete. That's because the Executed2 method is marked Async but we didn't mark the call with Await. We are not allowed to mark the call with Await because SyncToAsyncExecuted is not an Async method itself.

So although we are allowed to call an Async method without the Await operator, we must be aware that the calling code with continue immediately. Sometimes this is what we want, sometimes it is not.

The fourth button calls synchronous code from async code. 


Because most of the code executes synchronously we see we cannot move the window while it is running and the listbox is only updated once all the code has run. This is what we would expect. As you can see, there is no special syntax for calling synchronous methods from async methods.

Because AsyncToSyncExecuted2 is not marked as Async we don't have the option or need to call it with the await operator.

I hope this cleared a few things up for you.



No comments:

Post a Comment