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.
- Repository: Microsoft/vs-threading
- Developers should be aware of the Three Threading Rules
- Several analyzers are included to help identify problems early in the development process: https://github.com/Microsoft/vs-threading/blob/master/doc/analyzers/index.md
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:
- Refresh triggered by action on main thread
- Refresh operation runs asynchronously
- 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