ASP.NET HttpContext in async/await patterns using the Task Parallel Library – Part 3
The SynchronizationContext (SC) is a vital component for the ASP.NET request lifecycle and it is strictly coupled to the HttpContext. In the previous posts we saw how running asynchronous operations by using specific TPL patterns might lead to losing the SynchronizationContext and, thus, reference to the HttpContext.
Such a behavior can impact applications in several ways, both directly (e.g. tasks failing due to null references when accessing the HttpContext or one of its fields) and indirectly (e.g. faults in external libraries).
Before jumping to the summary and conclusions, I owe you an answer to the challenge from the previous post.
Below the question I left you to meditate upon, which refers to our demo MVC application used to output debug traces about the execution flow and the SC flow status (I encourage you to quickly re-check the previous posts to recall the code involved):
What do you expect to happen when calling the Index action with
configureAwait = false
tsFromSyncContext = true
Previously we went through some tests and analyzed what happens when turning those flags on and off individually. Specifically:
- configureAwait set to false causes the SC to stop flowing starting from the very first await operation. This yields into a definitive loss of the SC, which is then unavailable both in the awaited task and in the awaiter context once it resumes the execution.
- tsFromSyncContext set to true, instead, proved to be useful to persist the SC on the awaited task.
This is the output we get when running the application with both flags enabled.
Notice the second await operation (the one calling Task.Factory.StartNew) fails. The error message is somewhat cryptic. However if we speculate on what the consequences of calling ConfigureAwait(false) on the awaiter context are, we should understand the SC is lost once the execution is returned to the controller context. Therefore, when the second await operation is invoked and tries to post the newly created task against the current SC, the operation simply can't happen as there is no SC available any more.
If you followed me thus far, this example should point out how delicate this whole system could be and how the TPL primitives could be challenging to handle and lead to unexpected behaviors.
Wrapping it all up!
Up to this point, I hope we have a clearer vision on the various scenarios we can run into when using async/await patterns in ASP.NET. However, I bet there are still a few questions open. The most interesting ones, or better, the ones I would be asking myself are:
- Why awaiting on Task.Run (or other primitives like Task.Factory.StartNew) does not persist the SC while awaiting on a custom async method does?
- Why if we force the awaiter to use the SynchronizationContextTakScheduler we don't face this problem?
- Avoiding primitive TPL APIs might just not be a feasible option. How can we deal with this situation and instrument a workaround?
Let's try to address them one by one.
Why awaiting on Task.Run does not persist the SC while awaiting a custom async method does?
To find out, we need to peek at the .NET source code. Luckily, it's public! :)
Task.Run and other primitive TPL methods rely on the internal method Task.InternalStartNew, which then calls into the following one:
[code highlight="9" language="csharp"]
internal void PossiblyCaptureContext(ref StackCrawlMark stackMark) {
// In the legacy .NET 3.5 build, we don't have the optimized overload of Capture()
// available, so we call the parameterless overload.
#if PFX_LEGACY_3_5
CapturedContext = ExecutionContext.Capture();
#else
CapturedContext = ExecutionContext.Capture(
ref stackMark,
ExecutionContext.CaptureOptions.IgnoreSyncCtx | ExecutionContext.CaptureOptions.OptimizeDefaultCase);
#endif
}
The Capture method, invoked with the IgnoreSyncCtx flag, does exactly what we would expect. It skips capturing the SC, suppressing its flow.
[code highlight="2" language="csharp"]
// capture the sync context
if (0 == (options & CaptureOptions.IgnoreSyncCtx))
syncCtxNew = (ecCurrent.SynchronizationContext == null) ? null : ecCurrent.SynchronizationContext.CreateCopy();
Guess what? The framework doesn't let user-defined async methods to follow this very same path and simply lets the SC flow.
What this actually means is if in our code we simply run await on DoStuff, the SC never gets lost.
Why if we force the awaiter to use the SynchronizationContextTakScheduler we don't lose the SC?
This question also needs some internals lookup.
The SyncrhonizationContextTaskScheduler exposes a queueing mechanism that posts the work items against the currently captured SC. In this way, the SC keeps flowing and it is persisted.
[code highlight="3" language="csharp"]
protected internal override void QueueTask(Task task)
{
m_synchronizationContext.Post(s_postCallback, (object)task);
}
On the contrary, the ThreadPoolTaskScheduler (default for ASP.NET), ignores the SC completely:
[code highlight="14" language="csharp"]
protected internal override void QueueTask(Task task)
{
if ((task.Options & TaskCreationOptions.LongRunning) != 0)
{
// Run LongRunning tasks on their own dedicated thread.
Thread thread = new Thread(s_longRunningThreadWork);
thread.IsBackground = true; // Keep this thread from blocking process shutdown
thread.Start(task);
}
else
{
// Normal handling for non-LongRunning tasks.
bool forceToGlobalQueue = ((task.Options & TaskCreationOptions.PreferFairness) != 0);
ThreadPool.UnsafeQueueCustomWorkItem(task, forceToGlobalQueue);
}
}
Avoiding primitive TPL APIs might just not be a feasible option. How can we deal with this situation and instrument a workaround?
Eventually if we just cannot get rid of calling Task.Run, what can we do?
The best option is to plan for stateless asynchronous operations. If you think about it, the great advantage of async/await is to offload background operations and allow the request to keep on executing in the meantime. Making those tasks stateless not only solves the potential problem of losing the HttpContext, but it also makes the web application more robust in case a background operation fails.
Odds of a ASP.NET request really needing to await on a purely stateless operation are not many and should let you think twice whether background operations are worth to be made asynchronous at all, given they would still have to be awaited for the request to complete.
Last but not least if you are thinking of porting your web app on microservices for great scaling and orchestration (e.g. deploying it on a Azure Service Fabric cluster), stateless agents performing specific, request-agnostic tasks can be much easier to implement.
That said, there might be situations where going stateless is not possible or easily achievable. Similarly there certainly are scenarios where an ASP.NET request should await on an async operation.
For such situations, the most immediate approach is either to await on custom async methods (which do not lead to SC loss) or on new tasks by using the Task.Factory.StartNew overload accepting a TaskScheduler as parameter. In other words, this would mean overwriting all code references from
[code gutter="false" language="csharp"]
Await Task.Run(myBackgroundDelegate());
To
[code gutter="0" language="csharp"]
Await Task.Factory.StartNew(myBackgroundDelegate(),
CancellationToken.None,
TaskCreationOptions.RunContinuationsAsynchronously,
TaskScheduler.FromCurrentSynchronizationContext());
Unfortunately, this approach might not fit all the situations too. For instance what if Task.Run is used in an external library?
In such a scenario, there is actually one an additional "trick" we can use. The caveat is to await on a wrapper task, that manually posts the actual awaitable operation against the SC.
To grasp how this works a little better, let's see it applied to our very first example using the Context action of the Home controller.
Besides the code for storing the SC I added a new log line to show if the SC context is properly restored once the execution resumes on the controller context:
[code highlight="7, 11-17, 19" language="csharp"]
public async Task<ActionResult> Context()
{
Response.Write("
Context action invoked.<hr/>
<ul>");
Response.Write($"
<li>Current HttpContext in <strong>controller context</strong> is { System.Web.HttpContext.Current.ToString() }</li>
");
//first we save the current SC
var sc = System.Threading.SynchronizationContext.Current;
//instead of awaiting on Task.Run(WriteHttpContext()), we use a wrapper
//the SC is therefore persisted both within the awaited and awaiter
await Task.Run(() =>
{
sc.Post((state) =>
{
WriteHttpContext().Invoke();
}, null);
}).ConfigureAwait(configureAwait);
Response.Write($"
<li>Current HttpContext in <strong>controller context</strong> is { System.Web.HttpContext.Current.ToString() }</li>
");
Response.Write($"</ul>
<hr/>Response complete.");
return new HttpStatusCodeResult(200);
}
If you recall when we walked through this sample in the first post, we saw the HttpContext being lost in the awaited Task context. Here's the output we get this time:
If we were to use this approach with the Index action instead and get the full verbose output, we would see the SC being of type AspNetSynchronizationContext and the task scheduler of type ThreadPoolTaskScheduler.
Why is that? Simply because the "posting" against the SC is not piloted by the task scheduler in this case, but it is manually requested. In other words, we are explicitly taking care of what the AspNetSynchronizationContextTaskScheduler would do for us!
I know, this whole story is kind of endless and can get seriously confusing at times. However, I hope this posts contains enough insight to let you understand the basic inner workings between the TPL and the ASP.NET HttpContext.
As already stressed out, I believe a code sample is worth more than a thousand words. Although the demo web app I used is not at all complicated and it is fairly easy to setup, I will soon update this post to include the link to my GitHub repo project, so you can just clone it and mess around with it!
One final word to thank all the people who actively contirbute to this blog. I am humbled and impressed by receiving your feedback and I appreciate your efforts in highlighting possible mistakes or confused explanations! Also, thanks in advance to the ones who I am sure will be helping in the future too!
Happy parallel HttpContexting! :)
Catch you on the flipside!
albigi / Alessandro Bigi
Comments
- Anonymous
January 26, 2019
Thanks friis,It was very helpful in order to understand this. In fact i was frustrated to figure out why HttpContext.Current was resulting in null in async await approach when we use Task.Run().