Wednesday, November 17, 2021

Async/Await

Resources

For me, a glaring omission in the above is how a non-GUI thread is supposed to work with async calls provided by another library. And how to avoid deadlock. My first use of async was with websockets, and right away I had deadlock. It seems that lots of people on stackoverflow have this same problem. It seems that ConfigureAwait is the solution. But it's not clear to me how it can be safely used. Also it's not clear how to terminate an async stack when you can't make the top-level call async. But a solution for that is given in stackoverflow as well.

Summary

Working with the breakfast analogy:

private static async Task<Egg> FryEggsAsync(int howMany)
{
  Console.WriteLine("Warming the egg pan...");
  await Task.Delay(3000);
  Console.WriteLine($"cracking {howMany} eggs");
  Console.WriteLine("cooking the eggs ...");
  await Task.Delay(3000);
  Console.WriteLine("Put eggs on plate");            
  return new Egg();
}
  • The async keyword in the signature just means that the body can use await. It doesn't cause any non-synchronous behaviour to occur.
  • Task<Egg> in the signature means that the body must return an Egg. In practice, this function will be partially executed, later resumed, and finally completed, returning an Egg. However when it is partially executed, and comes to a long-running part, it really does return, and it returns the Task which can communicate progress through many resumptions and final completion of the function.
  • Similarly, Task in the signature would mean that the body must return nothing, so you may not have any explicit return even though your signature says you return a task. This is because async methods really have two return types. There is the Task that communicates progress through resumptions and the final product of the work.
// short form
await Task.Delay(3000);

// long form
Task task = Task.Delay(3000); // start a job
await task; // get the output of a job

// short form
Egg egg = await FryEggsAsync(2);

// long form
Task task = FryEggsAsync(2);
Egg egg = await task;
  • Executing a "function that returns Task" starts some processing.
  • Awaiting a task extracts the final product of the work from the Task which implies that we have waited for the processing to complete, but await doesn't mean block, it means "proceed with other work in the mean time".
  • So in the body of FryEggsAsync, our thread prints "warming", then starts a delay, then goes elsewhere, and some time later comes back and prints "cracking". Where does it go? All the way up the stack.

Async All the Way

class Program
{
  static async Task Main(string[] args)
  {
      Console.WriteLine("starting");
      var eggsTask = FryEggsAsync(2);
      var baconTask = FryBaconAsync(3);

      Console.WriteLine("waiting");
      var eggs = await eggsTask;
      var baconTask = await baconTask;

      Console.WriteLine("done");
  }
}

It's tempting to think that it just goes to the next line after FryEggsAsync, which it does, but then you always hit another await, so we've only deferred the question. For a console Main, there is some compiler magic. For a GUI app, your thread goes back to its dominant task, which is pumping the queue of user initiated events to which it must respond. That works great for John's example:

Old Way:

public class MyService
{
  public void StartCostlyWork()
  {
    // Start something in another thread and return.
    // When that thing is done, that thread will trigger the ProcessCompleted event.
    // Starting the work must not throw exception.
  }

  public event EventHandler ProcessCompleted;
}

public class MyViewModel
{
  private MyService service;

  public MyViewModel()
  {
    service = new MyService();
    service.ProcessCompleted += OnMyServiceProcessCompleted;
  }

  public void RunOnGuiThreadFromButtonClick()
  {
    myService.StartCostlyWork();
  }

  // This is triggered regardless of success/failure. Status is reported in the outcome.
  public void OnMyServiceProcessCompleted(object sender, EventArgs outcome)
  {
    // In order to interact with the UI we have to get back to the UI thread.
    Dispatcher.BeginInvoke(() =>
    {
      // Do some UI stuff based on the outcome.
    });
  }
}

New Way:

public class MyService
{
  public async Task<Outcome> DoCostlyWorkAsync()
  {
    // Do something costly.
    // Due to internal await calls, the thread can partially complete and resume as needed.
    // Everything that happened before on the other thread and that was passed to
    // ProcessCompleted now happens here.
  }
}

public class MyViewModel
{
  private MyService service;

  public MyViewModel()
  {
    service = new MyService();
  }

  public async Task RunOnGuiThreadFromButtonClick()
  {
    // It's now possible to let the processing throw errors instead of packaging
    // that into the outcome, and we no longer need the Dispatcher because
    // by default await keeps us on the same thread.
    try
    {
      Outcome outcome = await service.DoCostlyWorkAsync();
      // Do some UI stuff based on the outcome.
    }
    catch (Exception exception)
    {
      // Do some UI stuff based on the outcome.
    }
  }    
}

That's fine, but what if I'm not a view model, and I have to call MyService.DoCostlyWorkAsync? Say I'm a library and I have to call HttpClient.PostAsync and I can't control my entry point?

public interface IWebHelper
{
  void StartCostlyWork();
}

I'm a component in a server. I have to implement IWebHelper and can't change the interface. I don't know who is calling me. It might be another thread. It's probably another application. It might be .NetRemoting, or gRPC. I have to support a synchronous entry point.

public class WebHelper: IWebHelper
{
  public void StartCostlyWork()
  {
    // This starts the task and discards it. This notation isn't specific to async/await.
    // It just tells the compiler to not complain about assigning and not using a variable.
    _ = service.DoCostlyWorkAsync();
  }
}

That's not great because it means that you don't care if the task succeeds. That might be ok if nothing ever threw exceptions, but losing exceptions to the void is a terrible practice. For details, see the "Lost Exception Example" below.

Calling Async From Sync

stackoverflow and microsoft explain some hacks to call Async from Sync. But these are hacks.

The blocking hack ".GetAwaiter().GetResult()" won't help because it requires that ConfigureAwait(false) is used everywhere, even in dependent libraries. HttpClient for example captures the context on some platforms, so in general this won't work.

The thread pool hack seems like the only viable option for a server component that needs to call async helpers from a fixed non-async stack called by some unknown possible via remoting (i.e. IPC).

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    var task = Task.Run(() => AsyncContext.Run(() => GetAsync(id)));
    return task.GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "https://www.example.com/api/values/" + id);
  }
}

In this code we offload the asynchronous work to the thread pool, then block on the resulting task. Task.Run executes the asynchronous method on a thread pool thread. Here it will run without a context, thus avoiding the deadlock. Without a specific context, it can’t access UI elements or the ASP.NET HttpContext.Current, but we don't have those in an opaque server-component environment.

Another issue is that the asynchronous method may resume on any thread pool thread. This is a problem if the method uses per-thread state or if it implicitly depends on the synchronization provided by a UI context. We can't know if dependent libraries will use per-thread state, so this problem is avoided by the use of AsyncContext.Run which forces the asynchronous code to resume on the same thread. But for this you need the Nito.AsyncEx.Conent NuGet, so I'm not using it yet.

I converted the "Lost Exception Example" to "Server Async-From-Sync" to show it now catches exceptions. I couldn't use the ShowMessage hack. That caused deadlock. I'm not sure why, but that's ok because the whole point is for this to be used by non-UI code.

Re-Entrant

But we're not done. Something none of the guides mention is re-entrance. This applies to the normal GUI examples as well as my async-from-sync server scenario. In any case, when the thread "goes elsewhere", that means it could process a request that re-enters the same task. I the breakfast example, if it's a GUI instead of a Console Main (which is the whole point of async) then there's probably a button "StartBreakfast" or "FryEggs" and if that button interacts with a member variable, everything can get confused.

That's just a standard multi-tasking problem, but it needs different solutions in the async world. For example you can't await inside a lock. But this doesn't effect my server example much since the entry points were already of the StartSomeProcess nature and already had to FailIfAlreadyStarted.

Lost Exception Example

In this example, the first click runs the task which via the ShowMessage hack shows how the background task successfully runs to completion despite being discarded. The next time you click, the task throws exception, but your app won't notice. Your debugger will notice. And you could probably configure some app-wide catcher. But for an isolated server component this isn't acceptable. Somewhere we must at least catch and log exceptions.

MainWindow.xaml

<Window
  x:Class="AsyncAwaitTestGUI.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:local="clr-namespace:AsyncAwaitTestGUI"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  Width="900"
  Height="600"
  mc:Ignorable="d">
  <DockPanel>
    <Label Name="StatusLabel" Content="Status: Unknown" DockPanel.Dock="Top"/>
    <Button Name="DoWorkButton" Content="DoWork" Click="DoWorkButton_Click" DockPanel.Dock="Top"/>
    <TextBox Name="MessagesTextBox" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" Height="auto" DockPanel.Dock="Top"/>
  </DockPanel>
</Window>

MainWindow.xaml.cs

namespace AsyncAwaitTestGUI
{
    using System;
    using System.Threading.Tasks;
    using System.Windows;

    public partial class MainWindow : Window
    {
        private MandatorySyncEntry syncEntry = new MandatorySyncEntry();

        public MainWindow()
        {
            this.InitializeComponent();
            this.Title = "AsyncAwaitTestGUI";
            MainWindow.DoShowMessage = this.ShowMessageImpl;
        }

        private void DoWorkButton_Click(object sender, RoutedEventArgs e)
        {
            string status = "Hello";
            if (this.StatusLabel.Content as string == status)
            {
                status = "World";
            }

            this.syncEntry.StartCostlyWork(status);
            this.StatusLabel.Content = status;
        }

        private void ShowMessageImpl(string message)
        {
            this.Dispatcher.Invoke(() =>
            {
                this.MessagesTextBox.Text = message;
            });
        }

        public static Action<string> DoShowMessage = null;

        public static void ShowMessage(string message)
        {
            if (DoShowMessage != null)
            {
                DoShowMessage(message);
            }
        }
    }

    public class MandatoryAsyncService
    {
        public async Task<Outcome> DoCostlyWorkAsync(string input)
        {
            MainWindow.DoShowMessage($"Starting DoCostlyWorkAsync({input})");
            var outcome = new Outcome();
            await Task.Delay(1000);
            if (input == "Hello")
            {
                outcome.Success = true;
            }
            else
            {
                throw new InvalidOperationException("MyFailure");
            }
            MainWindow.DoShowMessage($"DoCostlyWorkAsync({input}) returns {outcome.Success}");
            return outcome;
        }
    }

    public class MandatorySyncEntry 
    {
        private MandatoryAsyncService asyncService = new MandatoryAsyncService();

        public void StartCostlyWork(string input)
        {
            // This starts the task and discards it. This notation isn't specific to async/await.
            // It just tells the compiler to not complain about assigning and not using a variable.
            _ = asyncService.DoCostlyWorkAsync(input);
        }
    }

    public class Outcome
    {
        public bool Success = false;
    }
}

Server Async-From-Sync

In this example, the first click runs the task which via Debug.Write in the IDE console that the background task successfully runs to completion. The next time you click, the task throws exception, and is successfully caught and logged at the point of entry.

I think this solution also makes ConfigureAwait(false) unnecessary, since this technique means it will always run without a context, thus avoiding deadlock.

namespace AsyncAwaitTestGUI
{
    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Windows;

    public partial class MainWindow : Window
    {
        private MandatorySyncEntry syncEntry = new MandatorySyncEntry();

        public MainWindow()
        {
            this.InitializeComponent();
            this.Title = "AsyncAwaitTestGUI";
        }

        private void DoWorkButton_Click(object sender, RoutedEventArgs e)
        {
            string status = "Hello";
            if (this.StatusLabel.Content as string == status)
            {
                status = "World";
            }

            this.syncEntry.StartCostlyWork(status);
            this.StatusLabel.Content = status;
        }
    }

    public class MandatoryAsyncService
    {
        public async Task<Outcome> DoCostlyWorkAsync(string input)
        {
            Debug.Write($"Starting DoCostlyWorkAsync({input})\n");
            var outcome = new Outcome();
            await Task.Delay(1000);
            if (input == "Hello")
            {
                outcome.Success = true;
            }
            else
            {
                throw new InvalidOperationException("MyFailure");
            }
            Debug.Write($"DoCostlyWorkAsync({input}) returns {outcome.Success}\n");
            return outcome;
        }
    }

    public class MandatorySyncEntry 
    {
        private MandatoryAsyncService asyncService = new MandatoryAsyncService();

        public void StartCostlyWork(string input)
        {
            try
            {
                Task.Run(() => asyncService.DoCostlyWorkAsync(input)).GetAwaiter().GetResult();
            }
            catch (Exception exception)
            {
                Debug.Write($"StartCostlyWork({input}) catches {exception}\n");
            }
        }
    }

    public class Outcome
    {
        public bool Success = false;
    }
}
c#
{ "loggedin": false, "owner": false, "avatar": "", "render": "nothing", "trackingID": "UA-36983794-1", "description": "", "page": { "blogIds": [ 717 ] }, "domain": "holtstrom.com", "base": "\/michael", "url": "https:\/\/holtstrom.com\/michael\/", "frameworkFiles": "https:\/\/holtstrom.com\/michael\/_framework\/_files.4\/", "commonFiles": "https:\/\/holtstrom.com\/michael\/_common\/_files.3\/", "mediaFiles": "https:\/\/holtstrom.com\/michael\/media\/_files.3\/", "tmdbUrl": "http:\/\/www.themoviedb.org\/", "tmdbPoster": "http:\/\/image.tmdb.org\/t\/p\/w342" }