Async guidelines - psjw12/gitextensions GitHub Wiki

Starting with v3.0 the project is using the Microsoft/vs-threading library. The .NET Tasks implementation is very good at many things, but it has some serious limitations when it comes to two specific areas:

  • Applications that have a dedicated UI thread
  • Applications that mix synchronous and asynchronous code

In the presence of the above, applications are prone to failures with one of the following root causes:

  • Deadlock involving the UI thread
  • Logic errors due to reentrancy during a blocking wait

The vs-threading library, approach and analyzers make it substantially more difficult for errors in these categories to manifest at runtime. If this isn't already the new normal for applications that have a GUI thread, it will be soon. The logic behind the library is already hardened, and performance is proven at the scale of Visual Studio.

Please start with reading the following: https://github.com/Microsoft/vs-threading/blob/master/doc/cookbook_vs.md


Q: When to use SwitchTo(alwaysYield: true)? Is there a general rule saying when it should be used?

A: When you call an asynchronous method, the part of the method up to the first await always runs synchronously. To avoid this behavior, Task.Run can be used to force the operation to start on a different thread (or be queued). Since you can't synchronously block on it later (per the 3rd rule) - you instead switch to using Run or RunAsync to start asynchronous operations (or in one of its callers), and then you can await/Join on that later. JoinableTaskFactory.RunAsync behaves like other asynchronous methods, and doesn't yield prior to an explicit await. By calling SwitchTo, you do two things here:

  • you guarantee that the code afterwards is executing on the thread pool
  • If the code was already on the thread pool, you force the operation to yield and not continue synchronously

When switching from Task.Run to JoinableTaskFactory.Run (or RunAsync), the SwitchTo call is required to preserve semantics. If you don't need to force the code to execute as a separate operation, SwitchTo without the extra argument is more efficient.

. . .

Q: Is it ok for a public API to return JoinableTask?

A: If this is actually public API in an assembly that can be referenced from the outside, it is recommended to avoid exposing JoinableTask anywhere. Instead, make this a method that returns a Task. You can store the JoinableTask as a private field, and in this method call JoinableTask.JoinAsync() and return the result (or just await the JoinableTask).

. . .

Q: Is it ok to use ConfigureAwait with JoinableTaskFactory? E.g.:

ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
    var result = await DoWorkAsync().ConfigureAwait(false);
    // ...
});

A: We don't tend to need or use ConfigureAwait(false) when using JTF. If you do want to be on a background thread, this doesn't guarantee it since if the Task you're awaiting is already done, no yield will occur. If you really want to be on a background thread, await TaskScheduler.Default; does the trick, as you know. In a nutshell:

  • Use ConfigureAwait(false) before switching to a background thread and when staying on a background thread
  • Do not use ConfigureAwait before switching to the main thread or when staying on the main thread

If/when a synchronous invocation is changed to asynchronous, keep the following sequence in mind:

  1. Refresh triggered by action on main thread
  2. Refresh operation runs asynchronously
  3. At the end of the refresh operation, code switches back to the main thread and updates the UI.

In this sequence, a CancellationToken will need to be obtained in step 1 (via CancellationTokenSequence), and then provided in step 3 as an argument to SwitchToMainThreadAsync. This approach provides protection¹ against race conditions where multiple refresh operations are running, by ensuring the last UI update corresponds to the last refresh triggered in step 1. For most read-only "refresh" operations, this approach covers a user's expectations.

¹ This protection should probably have a name. It's not the only form of protection, and makes an assumption about the asynchronous operation which is critical to understand. Specifically, while it prevents the reordering of step 3 with respect to step 1, it does not prevent the reordering of step 2. If step 2 has a strict ordering requirement, e.g. a Reset API Key operation that displays the current API key as a one-time password, you definitely would not want to use this approach because it would allow a stale password to be shown in the UI.

. . .

The above information is predominantly collated from responses provided by @sharwell and @AArnott