I often see recommended for async library code, that we should use ConfigureAwait(false)
on all async calls to avoid situations where the return of our call will be scheduled on a UI thread or a web request synchronization context causing issues with deadlocks among other things.
One of the problems with using ConfigureAwait(false)
is that it isn't something you can just do on the entry point of your library call. In order for it to be effective it must be done all the way down the stack throughout your library code.
It seems to me that a viable alternative is to simply set the current synchronization context to null at the top-level public-facing entry points of the library, and just forget about ConfigureAwait(false)
. However, I don't see many instances of people taking or recommending this approach.
Is there anything wrong with simply setting the current synchronization context to null on the library entry points? Are there any potential problems with this approach (other than the possible insignificant performance hit of having the await post to the default synchronization context)?
(EDIT #1) Adding some example code of what I mean:
public class Program
{
public static void Main(string[] args)
{
SynchronizationContext.SetSynchronizationContext(new LoggingSynchronizationContext(1));
Console.WriteLine("Executing library code that internally clears synchronization context");
//First try with clearing the context INSIDE the lib
RunTest(true).Wait();
//Here we again have the context intact
Console.WriteLine($"After First Call Context in Main Method is {SynchronizationContext.Current?.ToString()}");
Console.WriteLine("\nExecuting library code that does NOT internally clear the synchronization context");
RunTest(false).Wait();
//Here we again have the context intact
Console.WriteLine($"After Second Call Context in Main Method is {SynchronizationContext.Current?.ToString()}");
}
public async static Task RunTest(bool clearContext)
{
Console.WriteLine($"Before Lib call our context is {SynchronizationContext.Current?.ToString()}");
await DoSomeLibraryCode(clearContext);
//The rest of this method will get posted to my LoggingSynchronizationContext
//But.......
if(SynchronizationContext.Current == null){
//Note this will always be null regardless of whether we cleared it or not
Console.WriteLine("We don't have a current context set after return from async/await");
}
}
public static async Task DoSomeLibraryCode(bool shouldClearContext)
{
if(shouldClearContext){
SynchronizationContext.SetSynchronizationContext(null);
}
await DelayABit();
//The rest of this method will be invoked on the default (null) synchronization context if we elected to clear the context
//Or it should post to the original context otherwise
Console.WriteLine("Finishing library call");
}
public static Task DelayABit()
{
return Task.Delay(1000);
}
}
public class LoggingSynchronizationContext : SynchronizationContext
{
readonly int contextId;
public LoggingSynchronizationContext(int contextId)
{
this.contextId = contextId;
}
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine($"POST TO Synchronization Context (ID:{contextId})");
base.Post(d, state);
}
public override void Send(SendOrPostCallback d, object state)
{
Console.WriteLine($"Post Synchronization Context (ID:{contextId})");
base.Send(d, state);
}
public override string ToString()
{
return $"Context (ID:{contextId})";
}
}
The execution of this will output:
Executing library code that internally clears synchronization context
Before Lib call our context is Context (ID:1)
Finishing library call
POST TO Synchronization Context (ID:1)
We don't have a current context set after return from async/await
After First Call Context in Main Method is Context (ID:1)
Executing library code that does NOT internally clear the synchronization context
Before Lib call our context is Context (ID:1) POST TO Synchronization Context (ID:1)
Finishing library call
POST TO Synchronization Context (ID:1)
We don't have a current context set after return from async/await
After Second Call Context in Main Method is Context (ID:1)
This all works like I would expect, but I don't come across people recommending libraries do this internally. I find that requiring every internal await point be called with ConfigureAwait(false)
is annoying, and even one missed ConfigureAwait()
can cause trouble throughout an application. This seems like it would solve the issue simply at the public entry-point of the library with a single line of code. What am I missing?
(EDIT #2)
Based on some feedback from Alexei's answer, it seems I hadn't consider the possibility of a task not being immediately awaited. Since the execution context is captured at the time of the await (not the time of the async call), that would mean the change to SynchronizationContext.Current
would not be isolated to the library method. Based on this it would seem that it should suffice to force a capture of the context by wrapping the internal logic of the library in a call that forces an wait. For example:
async void button1_Click(object sender, EventArgs e)
{
var getStringTask = GetStringFromMyLibAsync();
this.textBox1.Text = await getStringTask;
}
async Task<string> GetStringFromMyLibInternal()
{
SynchronizationContext.SetSynchronizationContext(null);
await Task.Delay(1000);
return "HELLO WORLD";
}
async Task<string> GetStringFromMyLibAsync()
{
//This forces a capture of the current execution context (before synchronization context is nulled
//This means the caller's context should be intact upon return
//even if not immediately awaited.
return await GetStringFromMyLibInternal();
}
(EDIT #3)
Based on the discussion on Stephen Cleary's answer. There are some problems with this approach. But we can do a similar approach by wrapping the library call in a non-async method that still returns a task, but takes care of resetting the syncrhonization context at the end. (Note this uses the SynchronizationContextSwitcher from Stephen's AsyncEx library.
async void button1_Click(object sender, EventArgs e)
{
var getStringTask = GetStringFromMyLibAsync();
this.textBox1.Text = await getStringTask;
}
async Task<string> GetStringFromMyLibInternal()
{
SynchronizationContext.SetSynchronizationContext(null);
await Task.Delay(1000);
return "HELLO WORLD";
}
Task<string> GetStringFromMyLibAsync()
{
using (SynchronizationContextSwitcher.NoContext())
{
return GetStringFromMyLibInternal();
}
//Context will be restored by the time this method returns its task.
}
await
in your library method) this question would be much better... Also I suspect you'll get your answer by the time you are done trying to write such code :) – Alexei LevenkovSynchronizationContext.Current
right after an await it would always be null (unless the context itself does something to restore itself). – Daniel Tabuencanull
) and not restore it thus making all other calls not related to your library to usenull
context after call to your library (unless caller explicitly protects they context withawait
which is not requirement). – Alexei Levenkovasync
call to be immediatelyawait
-ed, but it is not the case. I.e. standard way of running code in parallel is to collect tasks first and thanawait
wit hWhenAll
. – Alexei Levenkov