ASP.NET HttpContext in async/await patterns using the Task Parallel Library – Part 2

In case you don't recall the previous post, or just landed here, make sure to skim through it before continue reading.

We left with some very boring theoretical discussions. As promised, we will get to the juicy bits this time!

Investigation - Part 1

Let's focus on the sample controller we built and give it a few spins:

Run 1

Red numbers represent calls to the Log method from the MVC controller itself. Blue numbers, instead, are calls to the Log method originating within a Task.

Let's analyze the flow:

  1. The controller executes on thread 79. Nothing remarkable here. All the information is available.
  2. The controller awaits DoStuff, which is scheduled to run on the same thread. No information is lost here
  3. We are back executing the controller. The thread is now 77 as the previous one was disengaged while awaiting the async call. No information is lost here as the SC is flown to the new thread
  4. Here we are awaiting again, on the StartNew API this time. Note at this stage DoStuff is again executed on thread 79, but the SC is no longer available (it has not flown to the new thread). We do get information loss here as the HttpContext is also no longer available
  5. When the execution resumes in the controller, we are back to normal.

At this stage I believe everyone should have clear one thing: if DoStuff needed to access the HttpContext (e.g. reading the session on the inbound cookies), we would have got a NullReferenceException!

Run 2

I know my readers are quick and they know exactly what happens if we run the code using ConfigureAwait(false). However, for the discussion's sake, it actually took me a while to understand the output below. Hence I decided to include it in the post!

If you are familiar with using ConfigureAwait(false), this output shouldn't surprise you. If you are not (as I wasn't myself), check again this great post. :)

In a nutshell: ConfigureAwait(true) is the default behavior when awaiting a task. This causes the SC to be captured and once the awaited task completes, the continuation is "posted" against the previously captured SC. On the contrary, ConfigureAwait(false) does not capture the SC and suppresses its flowing upon the task completion.In our example this means the awaiter (controller context) resumes discarding the current SC once the first DoStuff task completes (step 3). The controller has therefore lost its SC (as we see in the following steps).

Note: when a task is posted against a SC, this essentially means the Post method of the SC is invoked. The Post method queues the task to be appropriately worked. As you can see, the whole capturing of and posting against the SC is completely independent from the EC flow and the task scheduler used. The SC exposes a higher abstraction layer: it doesn't care how tasks are scheduled and worked, it is agnostic with respect to the low level implementation details.


Investigation - Part 2

Let's start with a quick summary:

  • Awaiting on native TPL async APIs, such as Task.Run or TaskFactory.StartNew, causes the SC to be lost
  • Likewise, the SC is lost when awaiting on non-primitive async methods and ConfigureAwait(false) is used
  • If the SC is not flown, so doesn't the HttpContext and all the ASP.NET state

So far so good, I guess! We understood the SC is vital for the HttpContext to be persisted and if, for whatever reason, the SC doesn't flow along with the EC, we might end up getting a NullReference when trying to access the ASP.NET request state.

Well, how do we force the SC to flow along with our async code then?

Run 3

Let's take a look at what happens if we run the test page with the tsFromSyncContext switch set to true.

The issue is gone!! Black magic?! :)

 

From a code perspective, tsFromSyncContext switched on just changes this line of code

[code lang="csharp" highlight="5"]
await Task.Factory.StartNew(
async () => await DoStuff(doSleep, configAwait).ConfigureAwait(configAwait),
System.Threading.CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Current);

into:

[code lang="csharp" highlight="5"]
await Task.Factory.StartNew(
async () => await DoStuff(doSleep, configAwait).ConfigureAwait(configAwait),
System.Threading.CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSyncrhonizationContext())

 

Any guess what's happening?! Yes!! You got it, I know you do!

The line highlighted caused the awaiter to use the AspNetSyncrhonizationContextTaskScheduler instead of the ThreadPoolTaskScheduler. The former is obviously aware of the importance of the SC and this is why it ensures the current SC flows. The latter instead, as previously mentioned, is a hard worker: its main goal is to maximize throughput and performance, therefore it tries to leave behind as much overhead as possible, including the SC.


Before we jump into the next post for the conclusions and you start to get seriously bored by my writing, let me leave you with a challenge!

What do you expect to happen when calling the Index action with:

  • configureAwait = false
  • tsFromSyncContext = true

If you followed me so far, you should be able to figure it out. Or even better, run the web app and see it for yourself!

Honestly, it took me a while to properly understand what is going on and which line of code in our web app is accountable for what behavior.

 

The answer to the challenge in the next post, but just a quick hint…this is a tricky question! :)

 

albigi

Comments