When does Task.Run flow SynchronizationContext with ExecutionContext?
Asked Answered
E

1

5

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?

Earpiercing answered 23/10, 2013 at 17:25 Comment(0)
E
10

When does Task.Run flow SynchronizationContext with ExecutionContext?

Never.

The point of that article is that (the public API for) flowing ExecutionContext will flow SynchronizationContext. But Task.Run (and "pretty much any asynchronous operation whose core implementation resides in mscorlib") will never do this.

The paragraph starting with "My expectation is valid if" is hypothetical. He's describing what would happen if Task.Run use the public API for flowing ExecutionContext. This would cause problems if it did this. That's why it doesn't ever do this.

Estancia answered 23/10, 2013 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.