-
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]: Add Task.Run overload that avoids capturing the ExecutionContext #80000
Comments
Tagging subscribers to this area: @mangod9 Issue DetailsBackground and motivationToday, suppressing ExecutionContext flow when doing e.g. using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
ClassWithTimer? classWithTimer = null;
app.MapGet("/", () =>
{
// Kick off the timer on the first request
classWithTimer ??= new();
return Activity.Current!.Id;
});
app.Run();
class ClassWithTimer : IDisposable
{
private readonly PeriodicTimer _timer;
public ClassWithTimer()
{
_timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
Task.Run(DoSomething);
}
private async Task DoSomething()
{
while (await _timer.WaitForNextTickAsync())
{
Console.WriteLine($"Activity: {Activity.Current?.Id}");
}
}
public void Dispose() => _timer.Dispose();
} The above code will print the activity id from the first request in the background timer. PS: While answering this question, I realized how unsatisfactory all of the answers were to me 😅 : API Proposalnamespace System.Threading.Tasks;
[Flags]
public enum TaskCreationOptions
{
DoNotCaptureExecutionContext = 0x80
}
[Flags]
public enum TaskContinuationOptions
{
DoNotCaptureExecutionContext = 0x80
}
public class Task
{
public static Task UnsafeRun(Action action);
public static Task UnsafeRun(Action action, CancellationToken cancellationToken);
public static Task<TResult> UnsafeRun<TResult>(Func<TResult> function);
public static Task<TResult> UnsafeRun<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task UnsafeRun(Func<Task?> function);
public static Task UnsafeRun(Func<Task?> function, CancellationToken cancellationToken);
public static Task<TResult> UnsafeRun<TResult>(Func<Task<TResult>?> function);
public static Task<TResult> UnsafeRun<TResult>(Func<Task<TResult>?> function, CancellationToken cancellationToken);
} API Usageusing System.Diagnostics;
var activity = new Activity("Activity");
activity.Start();
Task.UnsafeRun(async () =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine(Activity.Current?.Id);
}
});
Console.ReadLine(); Alternative DesignsThere are several:
using System.Diagnostics;
var activity = new Activity("Activity");
activity.Start();
Task.Factory.StartNew(async state =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine(Activity.Current?.Id);
}
},
null,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach | TaskCreationOptions.DoNotCaptureExecutionContext,
TaskScheduler.Default);
Console.ReadLine();
RisksA more complex API?
|
Re: ExecutionContext.Suppress/Restore example, you generally need even more boilerplate if your code can be called by user code you don't control as Updated example using the pattern commonly used in runtimeusing System.Diagnostics;
var activity = new Activity("Activity");
activity.Start();
bool restoreFlow = false;
try
{
if (!ExecutionContext.IsFlowSuppressed())
{
ExecutionContext.SuppressFlow();
restoreFlow = true;
}
Task.Run(async () =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine(Activity.Current?.Id);
}
});
}
finally
{
if (restoreFlow)
{
ExecutionContext.RestoreFlow();
}
}
Console.ReadLine(); |
@MihaZupan yep, I kept the example simple, but yes, you would need that boilerplate |
How do I get this triaged 😄 |
You be patient. |
I've always wondered why this couldn't be a no-op. |
Still waiting for triage... |
I spoke offline with David about this. In .NET 8, instead of: Task.Run(DoSomething); you can now do: using (ExecutionContext.SuppressFlow())
Task.Run(DoSomething); and it'll do what you want. From my perspective, that's sufficient, in addition to the other approaches of using e.g. UnsafeQueueUserWorkItem. If we were going to add something here, I don't think we'd do So, we could add: [Flags]
public enum TaskCreationOptions
{
+ DoNotCaptureExecutionContext = ...,
}
[Flags]
public enum TaskContinuationOptions
{
+ DoNotCaptureExecutionContext = ...,
} but I'd prefer to hold off and just rely on |
Would it also make sense to make |
That's what I meant by in .NET 8 it'll do what you want. As of yesterday it no longer throws if it's already suppressed. |
Got it. Do you want me to close this or are you using this to track that the |
Found it #82912 |
Would it be worthwhile to document this new suggested pattern somewhere? Esp with the change that it no longer throws if already suppressed |
using System.Diagnostics;
var activity = new Activity("Activity");
activity.Start();
using (ExecutionContext.SuppressFlow())
{
Task.Run(async () =>
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine(Activity.Current?.Id);
}
});
}
Console.ReadLine(); |
Background and motivation
Today, suppressing ExecutionContext flow when doing
Task.Run
is more difficult than it needs to be and can be important when kicking off "fire and forget" work items. When running in ASP.NET Core, detaching from the incoming request context when issuing fire and forget tasks is important as there are async locals that are scoped to the incoming request. This could mess with the distributed tracing output as further http calls from the fire and forget task will show up as rooted underneath the initial request where that work originated.e.g.
The above code will print the activity id from the first request in the background timer.
PS: While answering this question, I realized how unsatisfactory all of the answers were to me 😅 :
https://www.reddit.com/r/dotnet/comments/zvm9xi/unbind_logs_from_http_request/
API Proposal
API Usage
Alternative Designs
There are several:
Only add the
TaskCreationOptions
andTaskContinuationOptions
enum value but not the overloads. This means that developers would need to drop down toTask.Factory.StartNew(..)
to use this feature.A boolean could be added to each of the Task.Run overloads.
Do nothing and tell people to use one of the other APIs to explicitly suppress the ExecutionContext flow:
ThreadPool.QueueUnsafeWorkItem
ExecutionContext.Suppress/Restore
Risks
A more complex API?
The text was updated successfully, but these errors were encountered: