Struct SyncContext
Used by public async
library methods to reset the current task
SynchronizationContext so that continuations won't be
marshalled back to the current thread which can cause serious problems
for UI apps.
Namespace: Neon.Tasks
Assembly: Neon.Common.dll
Syntax
public struct SyncContext
Remarks
This class was adapted from this blog post:
I renamed the structure, converted it into a singleton and added an optional Yield() call and a global mode to tune the operation for server vs. UI applications.
The async/await pattern is more complex than it seems because the code after the await may run on the same thread that performed the await in some circumstances (e.g. for UI applications) or on another thread in other environments. Library code needs to adapt to both situations.
UI platforms like WinForms, WPF, UXP,... require that all user interface manipulation happen on the UI thread and the synchronization context in these cases will be configured to have all awaits default to continuing on the calling (typically UI) thread to make it easy for developers to await a long running operation and then update the UI afterwards.
The problem for UI applications is that if the awaited operation internally awaits on additional operations (which is quite common), then each of the internal operations will also continue on the UI thread. This can be big problem because there's only one UI thread and continuing on the UI thread means that the operation needs to be queued to the application's dispatcher possibly leading to serious performance and usability issues.
This is less of a problem for console and server apps where awaited operations generally continue on any free threadpool thread, but Task scheduling can be customized so this isn't necessarily always the case.
As the blog post linked above describes, developers are encouraged to call ConfigureAwait(bool),
passing false
for every async
call where the result doesn't need to be marshalled back
to the original synchronization context. Non-UI class libraries typically don't care about this.
The problem is that to do this properly, MSFT recommends that you call Task.ConfigureAwait(false)
on EVERY async
call you made in these situations. This is pretty ugly and will be tough
to enforce on large projects over long periods of time because it's just too easy to miss one.
It's also likely that async library methods will be called serveral, perhaps hundreds of times by applications and it's a shame to require application developers to call ConfigureAwait(bool) everywhere rather than somehow having the library APIs handle this.
This struct
implements a custom awaiter that saves the current synchronization context and then
clears it for the rest of the current method execution and then restores the original context when
when the method returns. This means that every subsequent await
performed within the method will
simply fetch a pool thread to continue execution, rather than to the original context thread. To
accomplish this, you'll simply await Clear at or near the top of your
async methods:
The global Mode property controls what the Clear method actually does. This defaults to ClearOnly which turns Clear into a NOP which is probably suitable for most non-UI applications that reduce overhead and increase performance.
UI applications should probably set the ClearAndYield which prevents nested method continuations from running on the UI thread and also ensures that any initial synchronous code won't run on the UI thread either.
using Neon.Tasks;
public async Task<string> HelloAsync()
{
// On UI thread
await SyncContext.Clear;
// On background thread
SlowSyncOperation();
// On background thread
await DoSomthingAsync();
// On background thread
await DoSomethingElseAsync();
// On background thread
return "Hello World!";
}
public async Task Main(string[] args)
{
// Set a mode suitable for UI apps.
SyncContext.Mode = SyncContextMode.ClearAndYield;
// Assume that we're running on a UI thread here.
var greeting = await HelloAsync();
// On UI thread
}
This example sets the ClearAndYield mode
and then awaits HelloAsync()
which clears the sync context and
then performs a long running synchronous operation and then two async
operations. Note how all of the continuations in HelloAsync()
after the clear are running on a background thread but the continuation
after await HelloAsync()
is back to running on the UI thread.
This is pretty close to being ideal behavior.
Properties
Clear
await
this singleton to clear the current synchronization
context for the scope of the current method as a potential performance
optimization. The original context will be restored when the method
returns.
Declaration
public static SyncContext Clear { get; }
Property Value
Type | Description |
---|---|
SyncContext |
Remarks
You'll typically at or near the top of your method. This will look something like:
using Neon.Tasks;
public async Task<string> HelloAsync()
{
await SyncContext.Clear;
await DoSomthingAsync();
await DoSomethingElseAsync();
return "Hello World!";
}
note
Clear is not a method.
Awaiting this property clears the current synchronization context such
that the subsequent async
calls will each marshal back to threads
obtained from the thread pool and due to the compiler's async magic,
the original synchronization context will be restored before the
HelloAsync()
method returns.
The Mode property controls what awaiting Clear actually does. This defaults to ClearOnly which is probably suitable for most non-UI applications. UI applications will probably want to explicitly set ClearAndYield to help keep continations off the UI thread, which is often desirable.
IsCompleted
INTERNAL USE ONLY: Do not call this directly.
Declaration
public bool IsCompleted { get; }
Property Value
Type | Description |
---|---|
bool |
Mode
Used to control what Clear actually does. This defaults to ClearOnly which is probably suitable for most non-UI applications by reducing task overhead. UI application will probably want to set ClearAndYield to keep work from running on the UI thread.
This defaults to ClearOnly for server code, because we're writing more server applications than UI applications these days.
Declaration
public static SyncContextMode Mode { get; set; }
Property Value
Type | Description |
---|---|
SyncContextMode |
Methods
GetAwaiter()
INTERNAL USE ONLY: Do not call this directly.
Declaration
public SyncContext GetAwaiter()
Returns
Type | Description |
---|---|
SyncContext |
GetResult()
INTERNAL USE ONLY: Do not call this directly.
Declaration
public void GetResult()
OnCompleted(Action)
INTERNAL USE ONLY: Do not call this directly.
Declaration
public void OnCompleted(Action continuation)
Parameters
Type | Name | Description |
---|---|---|
Action | continuation | The continuation action. |