次の方法で共有


ASP.NET HttpContext in async/await patterns using the Task Parallel Library - Part 1

Hopefully brief but yet needed introduction

For a long time, when ASP and ASP.NET WebForms were the latest trend, web developers had to rely mainly on synchronous programming. After all, server computing can count on multithreading and it's free from UI thread freezes. While users don't normally accept a client application freezing the UI for more than a few seconds, they are more prone waiting for a webpage to load. Moreover, AJAX and all browser-driven web requests can be used to split the page loading across multiple requests, each one running synchronously on a dedicated server thread. As far as simple web pages are concerned, this approach still works fairly decently today. Asynchronous programming is not necessary to render trivial page content or push some JS code to the client browser. However, as web applications grow in complexity, background tasks are getting more and more vital. Asynchronous programming plays a key role in modern web applications, allowing them to run background operations without significant impacts for user requests being served at the same time.

The main advantages of offloading long-running operations are essentially two:

  1. The server is able to serve requests quicker, possibly informing the user with regular updates on the background processing status. This remarkably increases the user experience (I guess we are all familiar with that horrible feeling when we are like "should I click back or reload the page? Will I have to fill in the form all over again?")
  2. The server can offload the long-running operations, so that request threads are freed up and can be used to accommodate more incoming requests. This remarkably lowers the odds of starving the thread pool and eventually queue the requests or drop them with a service unavailable message.

Asynchronous programming is not new in .NET. However, starting with .NET Framework 4 the TPL (Task Parallel Library) has taken over the stage as it offers a broad and easy-to-use set of APIs to write asynchronous code. My own naïve definition for TPL is "the set of libraries introducing Tasks-related types and the async/await operators" (as I said, that is extremely naïve and oversimplified. Async experts, please accept my apologies!)

 

There are lots of great discussions and blog posts about the TPL and asynchronous programming patterns in .NET. A quick search should bring up the latest and coolest ones.

Although being familiar with the topic surely eases the reading of this post, I wouldn't say that is a strict requirement. Given the complexity and broadness of the topic, I will try to focus on a very specific problem.

 

This first post describes the initial problem and introduces all the main characters of our discussion. Two more poss will follow: the first to add more details about the investigation; the second to summarize the topic and wrap up all the findings of the previous posts.


The problem

it is simple, in its formulation. Randomly our web application throws a NullReferenceException while performing asynchronous operations. With further investigation and debugging we realize that, while handling a request, the System.Web.HttpContext.Current object is null.

The HttpContext object stores all the request-relevant data, including the pointers to the native IIS request object, the ASP.NET pipeline instance, the Request and Response properties, the Session (and many others). Without all of this information, we can safely tell our code is unaware of the request context it is executing into. Designing entirely stateless web applications is not easy and implementing them is indeed a challenging task. Moreover, web applications are rich of third party libraries, which are generally black boxes where unspecified code runs.

How could it be that such a fundamental object, the HttpContext, is lost during the request execution? What can we do if it happens in our code and what approach should we take if it's happening inconsistently in third party libraries?

Let's focus on an practical example and consider the code below:

[code lang="csharp" highlight="8"]
public class HomeController : Controller
{
public async Task<ActionResult> Context()
{
Response.Write("<br/>Context action invoked.<hr/><ul>");
Response.Write($"<li>Current HttpContext in <strong>controller context</strong> is { System.Web.HttpContext.Current.ToString() }</li>");

await Task.Run(WriteHttpContext());

Response.Write($"</ul><hr/>Response complete.");
return new HttpStatusCodeResult(200);
}
private Action WriteHttpContext()
{
return delegate ()
{
var context = System.Web.HttpContext.Current;
Response.Write($"<li>Current HttpContext in <strong> awaited Task context</strong> is " +
$"{ (context == null ? "null" : context.ToString()) }" +
$"</li>");
};
}
}

This is an action method on a MVC controller. In my tests I compiled it against .NET 4.7, but the behavior should be unchanged for any other supported version (currently, 4.5.2 or above).

The controller writes directly into the response output buffer and returns upon completion (no Views involved).

The core part is the code line highlighted: the controller pauses the execution while an async task is executed. In this example, the task just writes more data into the response output buffer. Although this is not really a consuming operation that would require a background task, I tried to keep the code as short and simple as possible. So bear with me and let's pretend it actually makes sense to execute WriteHttpContext asynchronously.

If you load the page you would see this output:

Blimey! :)

The HttpContext was lost, for real!


Some quick (yet due) theory

Boring. I know….but I really cannot figure out a way to explain the juicy bits without mentioning a few key concepts.

Let me introduce you some good friends from the TPL: the TaskScheduler, the ExecutionContext and the SynchronizationContext?

TaskWhat? ExecutionWho? :)

  • The TaskScheduler is exactly what you would expect. A scheduler for tasks! (I would be a great teacher!)
    Depending on the type of .NET application used, a specific task scheduler might be better than others. ASP.NET uses the ThreadPoolTaskScheduler by default, which is optimized for throughput and parallel background processing.
  • The ExecutionContext (EC) is again somehow similar to what the name suggests. You can look at it as a substitute of the TLS (thread local storage) for multithreaded parallel execution. In extreme synthesis, it is the object used to persist all the environmental context needed for the code to run and it guarantees that a method can be interrupted and resumed on different threads without harm (both from a logical and security perspective). The key aspect to understand is the EC needs to "flow" (essentially, be copied over from a thread to another) whenever a code interrupt/resume occurs.
  • The SynchronizationContext (SC) is instead somewhat more difficult to grasp. It is related and in some ways similar to the EC, albeit enforcing a higher layer of abstraction. Indeed it can persist environmental state, but it has dedicated implementations for queueing/dequeuing work items in specific environments. Thanks to the SC, a developer can write code without bothering about how the runtime handles the async/await patterns.

There are a few very important takeaways about the SC:

  • ASP.NET has its own one, named AspNetyncrhonizationContext
  • The AspNetSynchronizationContext is responsible for making the HttpContext current, that is setting the static propery System.Web.HttpContext.Current to the HttpContext object related to the current request being processed.
  • Unlike the EC, the SC does not have to flow from thread to thread. It can, but it is not strictly necessary. Whether it actually does flow or not depends on the specific implementation of the async/await pattern.

To extensively understand how all of these elements come into play and how they seamlessly work together, I strongly recommend you to read this great post.


Investigation - Part 0

I studied Telecommunications Engineering at university. A rather theoretical subject, I would say. Then my professional career led me to study Computer Science. One of the aspects I like most about it is the opportunity to challenge myself in problem-solving with a practical approach. In situations like this, I cannot deny some theoretical basis are of great value to understand the problem context. However, there is nothing like opening an IDE and writing down some code to test and assess what happens in real life. Luckily for us, this is a fairly quick task and all we have to do is re-decorate our sample web app:

[code lang="csharp" highlight="7,8,12-18"]
public async Task<ActionResult> Index(bool doSleep = false, bool configAwait = true, bool asyncContinue = true, bool tsFromSyncContext = false)
{
Response.Write($"{Log("Index called.")}<br/><hr/><br/>");
var currentContext = System.Threading.SynchronizationContext.Current;
try
{
await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait);

Response.Write($"{Log("await DoStuff completed.")}<br/><hr/><br/>");

await Task.Factory.StartNew(
async () => await DoStuff(doSleep, configAwait)
.ConfigureAwait(configAwait),
System.Threading.CancellationToken.None,
asyncContinue ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None,
tsFromSyncContext ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Current)
.Unwrap().ConfigureAwait(configAwait);

Response.Write($"{Log("await new Task completed")}<br/><hr/>");
}
catch (Exception e)
{
Response.Write($"{Log($"Error {e.Message}")}<br/>");
}

return new HttpStatusCodeResult(200);
}
static string Log(string msg)
{
var syncCtx = System.Threading.SynchronizationContext.Current;
var taskSchdlr = TaskScheduler.Current;
var httpCtx = System.Web.HttpContext.Current;
return $"{msg} <ul><li>" +
$"Thread: {System.Threading.Thread.CurrentThread.ManagedThreadId} </li><li>" +
$"Task scheduler: {taskSchdlr} </li><li>" +
$"Sync Context: {syncCtx} </li><li>" +
$"Http Context: {httpCtx} </li></ul>";
}

async Task DoStuff(bool doSleep = true, bool configAwait = true)
{
if (doSleep)
{
Response.Write($"{Log($"DoStuff called. configAwait is <strong>{configAwait.ToString()}</strong>. Sleep is called")}<br/>");
await Task.Run(async () => System.Threading.Thread.Sleep(SLEEPINTERVALMS)).ConfigureAwait(configAwait);
}
else
{
Response.Write($"{Log($"DoStuff called. configAwait is <strong>{configAwait.ToString()}</strong>. GetAsync is called")}<br/>");
await new System.Net.Http.HttpClient().GetAsync("https://www.microsoft.com").ConfigureAwait(configAwait);
}
}

Ok, I know, lots of re-decorations and debugging output. The juicy part is just the two lines highlighted above.

I don't think it is worth discussing on the controller implementation, but here's a quick reference to better understand the query string parameters:

  • configAwait: controls the ConfigureAwait behavior when awaiting tasks (read on for additional considerations)
  • tsFromSyncContext: controls the TaskScheduler option passed to the StartNew method. If true, the TaskScheduler is built from the current SynchronizationContext, otherwise the Current TaskScheduler is used .
  • doSleep: if True, DoStuff awaits on a Thread.Sleep. If False, it awaits on a HttpClient.GetAsync operation
    Useful if you want to test it without internet connection
  • asyncContinue: controls the TaskCreationOptions passed to the StartNew method. If true, the continuations are run asynchronously.
    Useful if you plan to test continuation tasks too and to assess the behavior of task inlining in case of nested awaiting operations (doesn't affect LegacyASPNETSynchronizationContext)

 

Alright, I think I can safely admit this is possibly one of the most verbose posts of mine. Time to wrap it up and call it a day as we are going to deep dive into all of the above in the next post!

 

albigi

Comments

  • Anonymous
    December 04, 2017
    The comment has been removed
    • Anonymous
      December 05, 2017
      Hi Daniel,well spotted. Actually I am using a different overload of StartNew which simply returns a Task.public Task StartNew(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler);There are however overloads returning Task which would require an unwrap first.