-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Proposal: ExecutionContext.Run<TContext> overloads #30867
Comments
We effectively already have this internally (I added it to support ManualResetValueTaskSourceCore). But it also highlights that the state should be passed as ref, not in, to support mutating such state without allocation. |
Also, I think the generic parameter should be TState rather than TContext, since it could otherwise imply it's somehow the type of the ExecutionContext. And we should consider whether we actually want a new generic ContextCallback delegate, or instead add a more general purpose Action (this is just about the delegate name and namespace). |
Made those changes. I'd also like a protected override Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application, TContext context)
{
var s = (application, context);
return ExecutionContext.Run(_executionContext, state =>
{
return state.Appliation.ProcessRequestAsync(state.Context);
},
ref s);
} |
I'd prefer to keep the number of additional overloads here smaller, to avoid the bloat that comes from introducing additional generic methods and the necessary plumbing internally (ExecutionContext.Run is not a small method), etc, and just add the single void-returning overload. This is an advanced class that very little code needs to use directly, and it's ok to have more advanced use cases be a little more cumbersome in exchange for reduced bloat. Your desired ProcessRequestAsync could be done almost as easily with the void-returning overload: protected override Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application, TContext context)
{
var s = (application, context, (Task)null);
ExecutionContext.Run(_executionContext, (ref state) => // the ref state would actually need the declaring tuple type here, but that's the case in your sample code as well ;)
{
state.Item3 = state.application.ProcessRequestAsync(state.context);
}, ref s);
return s.Item3;
} |
What's the harm in adding another overload? Is it just because it's an advanced API?
After discovering quirks with async locals last night, more code needs to call this than I originally thought does need to call it (or use an async state machine). Also,I think we need ContextCallback to allow passing the state by ref |
A gigantic and complicated method in Corelib duplicated again (https://github.com/dotnet/coreclr/blob/09b000f7e734e2744892f482242fe6bb66c60d59/src/System.Private.CoreLib/shared/System/Threading/ExecutionContext.cs#L204-L271), an additional public delegate type, additional public API surface area for niche functionality that can be accomplished without that surface area, etc. |
That's why I wrote "add a more general purpose Action (this is just about the delegate name and namespace)", i.e. namespace System
{
public delegate void Action<T>(ref T arg);
} |
Fair enough, it just goes to show that APIs that take callbacks usually need to account for all the ways code can be called (that includes return values).
ActionRef? Does it need to be general purpose? |
Not sure. Just something to consider. |
Often I want to run with the current context; but prevent spilling (which I think @davidfowl is also trying to do). At the moment you need to do ExecutionContext.Run(ExecutionContext.Capture(), ... Would be good if you could avoid the capture and the EC Run could just use the current one; rather than converting it from its internal state; then passing it back in, when it converts it back to its internal state. |
public sealed class ExecutionContext
{
public static void Run(ContextCallback callback, object? state);
public static void Run<TState>(ContextCallback<TState> callback, ref TState state);
...
} Which could then be utilised internally; if desired: internal static class AsyncMethodBuilderCore
{
public static void Start<TStateMachine>(ContextCallback<TStateMachine> callback, ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // TStateMachines are generally non-nullable value types, so this check will be elided
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
ExecutionContext.Run(callback, ref stateMachine);
}
} |
What overhead are you talking about that could be avoided, and you have measurements to show that it actually matters when using in any of these consuming scenarios? |
And because of flow suppression you need to check if the EC is null before using Run... but Flow suppressed gets changed into So it gets complicated; because you can't set EC directly (which is fine); but an api to say run this on the current context, then throw away its changes when you return would be good. Much like |
API review - approved. Please match parameter names for the new overloads to parameter names for the existing overloads. |
Please do not forget to include the proposed API in some version of the NetStandard, to avoid this kind of problem: dotnet/standard#1700 |
@stephentoub Re:
There is this proposal aiming to relax that restriction: dotnet/csharplang#338 |
@stephentoub should this be marked up for grabs |
@davidfowl, would this be used in ASP.NET anywhere? I see only a single place in dotnet/runtime where this would be used to avoid an allocation, and it's on a path that will practically never be used; there are a few call sites it could be used to avoid a cast, but it's not clear that would actually help perf and it would have only a minor readability benefit (for code paths that are already using the advanced ExecutionContext.Run). I similarly see only a single call site to ExecutionContext.Run in all of ASP.NET, and that call site again already has a reference type argument so it would only help to avoid a cast, at the expense of additional generics. |
Can't remember the exact use case; but it was to pass multi-state via a valuetuple? |
Yes... but my question is, where are we going to do that? The only place I see is on a path in Socket that should almost never be used (and if it is used, perf is unlikely to be a concern). Are there others (it's certainly possible I've overlooked some)? Obviously there APIs that will get use outside of dotnet/runtime and dotnet/aspnetcore, but ExecutionContext.Run is plumbing/infrastructure code, and if we don't have good use for this overload, it's unlikely to be more useful higher in the stack. |
I was required to have a custom version of the |
Separate from this issue, can you elaborate on why you needed a custom version of that?
And successfully. What benefit does this overload bring to the table for those uses? My point of referring to all the code in dotnet/runtime is that the vast, vast majority of them are able to use the existing overload successfully, including all but one of the uses outside of corelib (and that one as mentioned will basically never be used). I get that there are extreme cases where this overload is valuable (I'm the one who added the overload internally in the first place); ManualResetValueTaskSourceCore is one... but it's literally the only one in all of the .NET implementation. |
We fixed the issue in a different way so our immediate need is no longer immediate. You can put this in future |
It was going to be used in Kestrel to avoid allocating per request. We were in a state machine and had multiple things to pass (this and the httpcontext) to ExecutionContext.Run so that we ended up with a clean execution context per request. |
It currently now allocates for any request the suspends but via |
Ben, can you elaborate? You would use this method there to avoid an allocation? How? If that's actually the case, it's probably worth exposing. |
It was arranged differently before; however because it wasn't using The difficulty with using the regular The For this particular example The main issue was passing method state (locals/params) into the EC.Run; if it was just class state then you could just pass the class. |
Of course, I just don't see how that's applicable here. If this overload was exposed, how would this code change to avoid allocations? We'd be able to pass in both values without an allocation, but the method isn't asynchronous nor awaitable. |
Just looked again; previously the The two approaches were:
Went with (2) as the overload didn't exist and it looked like ValueTask pooling was happening... |
Thanks. So will it change to (1) if this is added? |
Would make sense; or you could enable The choice is essentially between:
(2) is much easier to write, a better pattern and easier to understand; but (1) saves on the allocations as it currently stands with no pooling |
I've yet to see sure-fire evidence that doing so actually moves the needle. Did I miss it? On top of that, ASP.NET itself doesn't appear to be safe for that: dotnet/aspnetcore#16876. And even if it was enabled, the cache is finite in size, so it would very likely still allocate. |
Someone should probably fix those 😅
I was quite surprised in my load testing (for our app rather than TE testing) that the cache was sufficient. Likely because all the suspensions were very short lived. If there are longer lived suspensions then the allocations would likely become noise against the suspension time. We essentially moved to almost 0 allocations under heavy load; except for that one pesky allocation in websockets... |
For a different spin on the api; could do a run on default context one; (which would also work here) which might have a wider value as its hard to get back to default/clean context rather than SuppressFlow namespace System.Threading
{
public sealed class ExecutionContext
{
// New
public static void RunOnDefault<TState>(ExecutionContext context, ContextCallback<TState> callback, ref TState state);
}
// New (it exists but as internal)
public delegate void ContextCallback<TState>(ref TState state);
} /cc @davidfowl |
I think I'd rather just expose an
Does that imply you're using the switch enabled in production? Can you share on the relevant issue (this one's getting off topic ;-) the benefits you're seeing that couldn't be achieved by e.g. tweaking GC configuration settings? I would love real-world examples where the switch is providing meaningful improvements that aren't otherwise achievable; I've just not been able to get those yet.
I don't see that happening for .NET 5. Too much code would likely break, both in .NET itself and out, and in subtle ways that only manifest in production. I'm relatively confident that dotnet/runtime is in good shape for it, in part because we're employing an analyzer to help ensure good ValueTask use, but I suspect many other codebases have some problematic usage, dotnet/aspnetcore included. Given that, if this API is exposed, will ASP.NET use it? |
K thinking this through, it's a bit complicated as there are 3+ async calls that need guarding, and would ideally only capture as a singleton and only use Run once so the logic becomes complicated if most of them don't run sync, and probably inefficient. The simplest api to use would be to flatten it out of a call back and just use a So you could do something like var ec = ExecutionContext.Capture();
while (!cancelled)
{
await OnStartAsync();
await ProcessRequestAsync();
await OnCompleteAsync();
ExecutionContext.Restore(ec);
} Would that be acceptable? |
Being explicit the api would be: public sealed class ExecutionContext
{
public static void Restore(ExecutionContext context);
} Then could push all the |
That would indeed be much cleaner than a callback API. |
@stephentoub do you want a new issue for #30867 (comment) or are you happy revisiting it with the approval state of this issue? |
New issue please :-) And it sounds like this one shouldn't be implemented. As part of that, I'd also like to understand if you believe that API could be used by ManualResetValueTaskSourceCore without a perf penalty, such that we'd then delete the internal variant of this. And whether Restore could be used internally to delete any other duplicative code, again without harming perf. |
Added a change for |
Added api request #38011 |
Closing as addressed by ExecutionContext.Restore |
Today to run code on a specific
ExecutionContext
, there's a non-generic Run method that takes aContextCallback
and object state. We should add a generic overload to boxing can be avoided (for e.g. when passing a ValueTuple):The text was updated successfully, but these errors were encountered: