Is it a high traffic website? One possible explanation might be that you're experiencing ThreadPool
starvation when you are not using ConfigureAwait(false)
. Without ConfigureAwait(false)
, the await
continuation is queued via AspNetSynchronizationContext.Post
, which implementation boils down to this:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask; // the newly-created task is now the last one
Here, ContinueWith
is used without TaskContinuationOptions.ExecuteSynchronously
(I'd speculate, to make continuations truly asynchronous and reduce a chance for low stack conditions). Thus, it acquires a vacant thread from ThreadPool
to execute the continuation on. In theory, it might happen to be the same thread where the antecedent task for await
has finished, but most likely it'd be a different thread.
At this point, if ASP.NET thread pool is starving (or has to grow to accommodate a new thread request), you might be experiencing a delay. It's worth mentioned that the thread pool consists of two sub-pools: IOCP threads and worker threads (check this and this for some extra details). Your GetReportAsync
operations is likely to complete on an IOCP thread sub-pool, which doesn't seem to be starving. OTOH, the ContinueWith
continuation runs on a worker thread sub-pool, which appears to be starving in your case.
This is not going to happen in case ConfigureAwait(false)
is used all the way through. In that case, all await
continuations will run synchronously on the same threads the corresponding antecedent tasks have ended, be it either IOCP or worker threads.
You can compare the thread usage for both scenarios, with and without ConfigureAwait(false)
. I'd expect this number to be larger when ConfigureAwait(false)
isn't used:
catch (Exception ex)
{
Log("Total number of threads in use={0}",
Process.GetCurrentProcess().Threads.Count);
return Json("myerror", JsonRequestBehavior.AllowGet); // really slow without configure await
}
You can also try increasing the size of the ASP.NET thread pool (for diagnostics purpose, rather than an ultimate solution), to see if the described scenario is indeed the case here:
<configuration>
<system.web>
<applicationPool
maxConcurrentRequestsPerCPU="6000"
maxConcurrentThreadsPerCPU="0"
requestQueueLimit="6000" />
</system.web>
</configuration>
Updated to address the comments:
I realized I was missing a ContinueAwait somewhere in my chain. Now it
works fine when throwing an exception even when the top level doesn't
use ConfigureAwait(false).
This suggests that your code or a 3rd party library in use might be using blocking constructs (Task.Result
, Task.Wait
, WaitHandle.WaitOne
, perhaps with some added timeout logic). Have you looked for those? Try the Task.Run
suggestion from the bottom of this update. Besides, I'd still do the thread count diagnostics to rule out thread pool starvation/stuttering.
So are you saying that if I DO use ContinueAwait even at the top level
I lose the whole benefit of the async?
No, I'm not saying that. The whole point of async
is to avoid blocking threads while waiting for something, and that goal is achieved regardless of the added value of ContinueAwait(false)
.
What I'm saying is that not using ConfigureAwait(false)
might introduce redundant context switching (what usually means thread switching), which might be a problem in ASP.NET if thread pool is working at its capacity. Nevertheless, a redundant thread switch is still better than a blocked thread, in terms of the server scalability.
In all fairness, using ContinueAwait(false)
might also cause redundant context switching, especially if it's used inconsistently across the chain of calls.
That said, ContinueAwait(false)
is also often misused as a remedy against deadlocks caused by blocking on asynchronous code. That's why I suggested above to look for those blocking construct across all code base.
However the question still remains as posted as to whether or not
ConfigureAwait(false) should be used at the top level.
I hope Stephen Cleary could elaborate better on this, by here's my thoughts.
There's always some "super-top level" code that invokes your top-level code. E.g., in case of a UI app, it's the framework code which invokes an async void
event handler. In case of ASP.NET, it's the asynchronous controller's BeginExecute
. It is the responsibility of that super-top level code to make sure that, once your async task has completed, the continuations (if any) run on the correct synchronization context. It is not the responsibility of the code of your task. E.g., there might be no continuations at all, like with a fire-and-forget async void
event handler; why would you care to restore the context inside such handler?
Thus, inside your top-level methods, if you don't care about the context for await
continuations, do use ConfigureAwait(false)
as soon as you can.
Moreover, if you're using a 3rd party library which is known to be context agnostic but still might be using ConfigureAwait(false)
inconsistently, you may want to wrap the call with Task.Run
or something like WithNoContext
. You'd do that to get the chain of the async calls off the context, in advance:
var report = await Task.Run(() =>
_adapter.GetReportAsync()).ConfigureAwait(false);
return Json(report, JsonRequestBehavior.AllowGet);
This would introduce one extra thread switch, but might save you a lot more of those if ConfigureAwait(false)
is used inconsistently inside GetReportAsync
or any of its child calls. It'd also serve as a workaround for potential deadlocks caused by those blocking constructs inside the call chain (if any).
Note however, in ASP.NET HttpContext.Current
is not the only static property which is flowed with AspNetSynchronizationContext
. E.g., there's also Thread.CurrentThread.CurrentCulture
. Make sure you really don't care about loosing the context.
Updated to address the comment:
For brownie points, maybe you can explain the effects of
ConfigureAwait(false)... What context isn't preserved.. Is it just the
HttpContext or the local variables of the class object, etc.?
All local variables of an async
method are preserved across await
, as well as the implicit this
reference - by design. They actually gets captured into a compiler-generated async state machine structure, so technically they don't reside on the current thread's stack. In a way, it's similar to how a C# delegate captures local variables. In fact, an await
continuation callback is itself a delegate passed to ICriticalNotifyCompletion.UnsafeOnCompleted
(implemented by the object being awaited; for Task
, it's TaskAwaiter
; with ConfigureAwait
, it's ConfiguredTaskAwaitable
).
OTOH, most of the global state (static/TLS variables, static class properties) is not automatically flowed across awaits. What does get flowed depends on a particular synchronization context. In the absence of one (or when ConfigureAwait(false)
is used), the only global state preserved with is what gets flowed by ExecutionContext
. Microsoft's Stephen Toub has a great post on that: "ExecutionContext vs SynchronizationContext". He mentions SecurityContext
and Thread.CurrentPrincipal
, which is crucial for security. Other than that, I'm not aware of any officially documented and complete list of global state properties flowed by ExecutionContext
.
You could peek into ExecutionContext.Capture
source to learn more about what exactly gets flowed, but you shouldn't depend on this specific implementation. Instead, you can always create your own global state flow logic, using something like Stephen Cleary's AsyncLocal
(or .NET 4.6 AsyncLocal<T>
).
Or, to take it to the extreme, you could also ditch ContinueAwait
altogether and create a custom awaiter, e.g. like this ContinueOnScope
. That would allow to have precise control over what thread/context to continue on and what state to flow.