Pregunta

My question was raised by one of the examples from this article:

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.

Let's say instead of the example in the article I have:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await DownloadAndComputeAsync(); 
}

// Can't be changed
private async Task<string> DownloadAndComputeAsync() {
    return await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

And for some reason I can't change the code in DownloadAndComputeAsync to use ConfigureAwait(false), perhaps because it's in a library or other code I don't control.

Is there a way I can suppress capture of the UI's SynchronizationContext as part of the ExecutionContext?

The article I linked above mentions an internal version of Task.Run that suppresses capture of the SynchronizationContext, but it's, well, internal. If it wasn't internal you could do something like this:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await Task.Run(DownloadAndComputeAsync, InternalOptions.DoNotCaptureSynchronizationContextInExecutionContext);
}

// Can't be changed
private async Task<string> DownloadAndComputeAsync() {
    return await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

Use cases

I don't have a case where I know I'm going to want/need to use this, but after reading the article I linked at the top I can think of a couple places where it might be useful.

  1. If from the UI context I'm using a library that doesn't use ConfigureAwait(false) and ends up doing significant work on the UI thread. Admittedly I'd probably stop using the library in this case if I could.

  2. The current recommended best practice seems to be one of the following

    1. Use ConfigureAwait(false) everywhere unless you know you're going to need to come back to the current context. The downside to this is that ConfigureAwait(false) is pretty ugly and affects code readability negatively (imo).
    2. Use ConfigureAwait(false) only in library code but be aware in all internal code whether or not a given async method might be used from e.g. the UI context and use ConfigureAwait(false) only where absolutely required. This seems less than desirable as it means classes must be aware of whether or not their async methods may ever be called on, e.g., the UI context.

    I was wondering if there might be a third possibility: require that in internal code any time an async method is called on, e.g., the UI context that the code in the event handler or similar is responsible for making sure that the async method won't have the UI context as its current context and therefore does not need to use ConfigureAwait(false) on the off chance that it's being called on the UI context. The upside to this is that all internal non-UI code does not need to use ConfigureAwait(false) even if it's called from an event handler and that all context aware code is limited to just UI code in, e.g., event handlers.

¿Fue útil?

Solución

First off; don't use an async delegate here. You're needlessly complicating your code.

When you have an operation that is already asynchronous that you want to get the result of, await it. If you have CPU bound work that you have to do then run just that in a Task.Run call and await it to get the result.

private void button1_Click(object sender, EventArgs e)  
{ 
    string data = await DownloadAsync(); 
    button1.Text = await Task.Run(() => Compute(data));
}

Here it's very clear what's going on. Get the result of the asynchronous operation, use it to do some CPU bound work in a non-UI thread, then use that result in the UI context.

Now, having said all of that, I'd expect your code to actually work just fine as it is (given that the current context is not the UI context all throughout the Task.Run delegate), but it's a lot harder to understand that code or see what it's doing.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top