I am trying to understand the impact of setting ConfigureAwait(false) in an async foreach loop?
await foreach (var item in data.ConfigureAwait(false))
{
//...
}
Exactly where do we lose the synchronization context? When would I want to set ConfigureAwait(false) inside the foreach look as above?
I understand the point of doing it in the async data source, but what about the foreach loop?
I would expect the context inside the loop to have no synchronization context, but my experiments show that the context is still there.
Here's my sample Winform app:
private async void button1_Click(object sender, EventArgs e)
{
var dataSource = new DataFactory();
IAsyncEnumerable<int> data = dataSource.GetData();
await foreach (var item in data.ConfigureAwait(false))
{
Console.WriteLine(item);
DataFactory.PrintContext("Data - item: " + item);
}
}
public class DataFactory
{
public async IAsyncEnumerable<int> GetData()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(250).ConfigureAwait(true);
PrintContext("Generating Data: " + i);
yield return i;
}
}
public static void PrintContext(string message)
{
Debug.WriteLine($"{message}, TID={Environment.CurrentManagedThreadId}, SyncContext.Current={(SynchronizationContext.Current?.GetType().Name?.ToString() ?? "null")}");
}
}
Sample output when I run the code:
Generating Data: 0, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Data - item: 0, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Generating Data: 1, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Data - item: 1, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Generating Data: 2, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Data - item: 2, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Generating Data: 3, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
Data - item: 3, TID=1, SyncContext.Current=WindowsFormsSynchronizationContext
The
GetDataiterator captures the ambient synchronization context on eachawait, because theawaitis configured withConfigureAwait(true). So the continuation after theawaitruns on the captured synchronization context. Since theGetDatais enumerated on the UI thread, the ambient synchronization context is theWindowsFormsSynchronizationContext, which is captured, so the continuation runs on the UI thread.Your expectation that using the external
ConfigureAwait(false)on the enumeration would somehow redirect the enumeration to theThreadPoolis not based. TheConfigureAwaitjust configures the capturing of the context. It's not a redirector/context-switcher. In case there is a context present, theConfigureAwait(false)will not capture it, but the context will stay there. TheConfigureAwait(false)is not a context-eliminator. It's not a call to action. It's a declaration of indifference. It basically says: "I don't care if a context exists, and I will act like it doesn't, even if it does."A common misconception is that in the absence of a synchronization context, the
awaitcontinuation is running on theThreadPool. In reality the continuation is running on whatever thread completed the awaitedTask. You can find an experimental demonstration of this fact in this answer.In case you want to run the enumeration on the
ThreadPool, you can just enclose the whole enumeration in aTask.Run:This way the
GetDataiterator will find no synchronization context to capture, making the internalConfigureAwait(true)irrelevant. The enumeration will start on theThreadPool(because of theTask.Run), and will stay on theThreadPoolbecause that's where theTask.Delayis completed by design. So it's coincidental. TheTask.Runby itself doesn't guarantee that the whole enumeration will happen on theThreadPool. It only affects where the enumeration starts.