This article from states that SynchronizationContext
may flow with ExecutionContext
:
private void button1_Click(object sender, EventArgs e) {
button1.Text = await Task.Run(async delegate
{
string data = await DownloadAsync();
return Compute(data);
});
}
Here’s what my mental model tells me will happen with this code. A
user clicks button1, causing the UI framework to invoke button1_Click
on the UI thread. The code then kicks off a work item to run on the
ThreadPool (via Task.Run). That work item starts some download work
and asynchronously waits for it to complete. A subsequent work item
on the ThreadPool then does some compute-intensive operation on the
result of that download, and returns the result, causing the Task that
was being awaited on the UI thread to complete. At that point, the UI
thread processes the remainder of this button1_Click method, storing
the result of the computation into the button1’s Text property.
My expectation is valid if SynchronizationContext doesn’t flow as part
of ExecutionContext. If it does flow, however, I will be sorely
disappointed. Task.Run captures ExecutionContext when invoked, and
uses it to run the delegate passed to it. That means that the UI
SynchronizationContext which was current when Task.Run was invoked
would flow into the Task and would be Current while invoking
DownloadAsync and awaiting the resulting task. That then means that
the await will see the Current SynchronizationContext and Post the
remainder of asynchronous method as a continuation to run back on the
UI thread. And that means my Compute method will very likely be
running on the UI thread, not on the ThreadPool, causing
responsiveness problems for my app.
The story now gets a bit messier: ExecutionContext actually has two Capture methods, but
only one of them is public. The internal one (internal to mscorlib)
is the one used by most asynchronous functionality exposed from
mscorlib, and it optionally allows the caller to suppress the
capturing of SynchronizationContext as part of ExecutionContext;
corresponding to that, there’s also an internal overload of the Run
method that supports ignoring a SynchronizationContext that’s stored
in the ExecutionContext, in effect pretending one wasn’t captured
(this is, again, the overload used by most functionality in mscorlib).
What this means is that pretty much any asynchronous operation whose
core implementation resides in mscorlib won’t flow
SynchronizationContext as part of ExecutionContext, but any
asynchronous operation whose core implementation resides anywhere else
will flow SynchronizationContext as part of ExecutionContext. I
previously mentioned that the “builders” for async methods were the
types responsible for flowing ExecutionContext in async methods, and
these builders do live in mscorlib, and they do use the internal
overloads… as such, SynchronizationContext is not flowed as part of
ExecutionContext across awaits (this, again, is separate from how task
awaiters support capturing the SynchronizationContext and Post’ing
back to it). To help deal with the cases where ExecutionContext does
flow SynchronizationContext, the async method infrastructure tries to
ignore SynchronizationContexts set as Current due to being flowed.
However it isn't exactly clear to me when this might happen. It appears that it will happen when the public ExecutionContext.Capture
method is used and the internal Task.Run
overload that suppresses flowing SynchronizationContext
with ExecutionContext
is not used, but I don't know when that would be.
In my testing on .NET 4.5 Task.Run
does not seem to flow the SynchronizationContext
with the ExecutionContext
:
private async void button1_Click(object sender, EventArgs e) {
Console.WriteLine("Click context:" + SynchronizationContext.Current);
button1.Text = await Task.Run(async delegate {
// In my tests this always returns false
Console.WriteLine("SynchronizationContext was flowed: " + (SynchronizationContext.Current != null));
string data = await DownloadAsync();
return Compute(data);
});
}
So my question is under what circumstances will Compute()
be run on the UI context (blocking the UI thread) as discussed in the article?
See Question&Answers more detail:
os