3

I'm having some trouble understanding the in's and out's of "continueOnCapturedContext" from a .NET v4.6 WebAPI 2 standpoint.

The problem I'm having is there doesn't appear to be any difference between ConfigureAwait(true) and ConfigureAwait(false).

I've put together a sample app that demonstrates what's happening:

    public async Task<IHttpActionResult> Get(bool continueOnContext)
    {
        int beforeRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;
        int runningExampleThreadId = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
        int afterRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;

        return Ok(new
        {
            HasSyncContext = SynchronizationContext.Current != null,
            ContinueOnCapturedContext = continueOnContext,
            BeforeRunningExampleThreadId = beforeRunningExampleThreadId,
            RunningExampleThreadId = runningExampleThreadId,
            AfterRunningExampleThreadId = afterRunningExampleThreadId,
            ResultingCulture = Thread.CurrentThread.CurrentCulture,
            SameThreadRunningAndAfter = runningExampleThreadId == afterRunningExampleThreadId
        });
    }

    private async Task<int> ExecuteExampleAsync(bool continueOnContext)
    {
        return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) => Thread.CurrentThread.ManagedThreadId).ConfigureAwait(continueOnContext);
    }

For "/Test?continueOnContext=true", this returns me:

{"HasSyncContext":true,"ContinueOnCapturedContext":true,"BeforeRunningExampleThreadId":43,"RunningExampleThreadId":31,"AfterRunningExampleThreadId":56,"ResultingCulture":"fr-CA","SameThreadRunningAndAfter":false}

So you can see I have a Sync context, I'm doing ConfigureAwait(true) and yet the thread isn't "continuing" in any way - a new thread is assigned before, while running and after running the asynchronous code. This isn't working the way I would expect - have I some fundamental misunderstanding here?

Can someone explain to me why in this code ConfigureAwait(true) and ConfigureAwait(false) are effectively doing the same thing?

UPDATE - I figured it out and have answered below. I also like the answer from @YuvalShap. If you're stuck on this like I was I suggest you read both.

Michael Clark
  • 462
  • 5
  • 17
  • 1
    ASP.NET implementation of synchronization context ensures that no two threads can enter the context at the same time. It doesn't ensure that the continuation will run on the same thread. You can find more detailed explanation here: http://vegetarianprogrammer.blogspot.co.il/2012/12/understanding-synchronizationcontext-in.html?m=1 – felix-b Feb 17 '18 at 10:46

2 Answers2

3

When an asynchronous handler resumes execution on legacy ASP.NET, the continuation is queued to the request context. The continuation must wait for any other continuations that have already been queued (only one may run at a time). When it is ready to run, a thread is taken from the thread pool, enters the request context, and then resumes executing the handler. That “re-entering” the request context involves a number of housekeeping tasks, such as setting HttpContext.Current and the current thread’s identity and culture.

From ASP.NET Core SynchronizationContext Stephen Cleary's blog post.

To sum up, ASP.NET versions prior to Core uses AspNetSynchronizationContext as the request context, that means that when you are calling ConfigureAwait(true) (or not calling ConfigureAwait(false)) you capture the context which tells the method to resume execution on the request context. The request context keeps HttpContext.Current and the current thread’s identity and culture consistent but it is not exclusive to a specific thread the only limitation is that only one thread can run in the context at a time.

YuvShap
  • 3,825
  • 2
  • 10
  • 24
  • Thanks for the extra info about the "housekeeping tasks" - that explains why setting the thread culture within the async operation gets ignored when returning back to the caller. Although I haven't been able to replicate it NOT "housekeeping" when ConfigureAwait(false) is specified - probably because the thread is switching when returning out. This answer and my own sum it up perfectly. I'll mark this as the answer because I imagine marking my own is poor form. – Michael Clark Feb 17 '18 at 20:03
2

OK I figured it out, so I'll post an answer in case it will help others.

In .NET 4.6 WebAPI 2 - the "Captured Context" that we are continuing on isn't the thread, it's the Request context. Among other things, the Request Context knows about the HttpContext. When ConfigureAwait(true) is specified, we're telling .NET that we want to keep the Request Context and everything about it (HttpContext & some other properties) after the await - we want to return to the context that we started with - this does not take into account the thread.

When we specify ConfigureAwait(false) we're saying we don't need to return to the Request Context that we started with. This means that .NET can just return back without having to care about the HttpContext & some other properties, hence the marginal performance gain.

Given that knowledge I changed my code:

    public async Task<IHttpActionResult> Get(bool continueOnContext)
    {
        var beforeRunningValue = HttpContext.Current != null;
        var whileRunningValue = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
        var afterRunningValue = HttpContext.Current != null;

        return Ok(new
        {
            ContinueOnCapturedContext = continueOnContext,
            BeforeRunningValue = beforeRunningValue,
            WhileRunningValue = whileRunningValue,
            AfterRunningValue = afterRunningValue,
            SameBeforeAndAfter = beforeRunningValue == afterRunningValue
        });
    }

    private async Task<bool> ExecuteExampleAsync(bool continueOnContext)
    {
        return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) =>
        {
            var hasHttpContext = HttpContext.Current != null;
            return hasHttpContext;
        }).ConfigureAwait(continueOnContext);
    }

When continueOnContext = true: {"ContinueOnCapturedContext":true,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":true,"SameBeforeAndAfter":true}

When continueOnContext = false: {"ContinueOnCapturedContext":false,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":false,"SameBeforeAndAfter":false}

So from this example you can see that HttpContext.Current exists before the asynchronous method and is lost during the asynchronous method regardless of the ConfigureAwait setting.

The difference comes in AFTER the async operation is completed:

  • When we specify ConfigureAwait(true) we get to come back to the Request Context that called the async method - this does some housekeeping and syncs up the HttpContext so it's not null when we continue on
  • When we specify ConfigureAwait(false) we just continue on without going back to the Request Context, therefore HttpContext is null
Michael Clark
  • 462
  • 5
  • 17