7

I noticed that CallContext.LogicalSetData/LogicalGetData don't work the way I expected them to do. A value set inside an async method gets restored even when there is no asynchrony or any kind of thread switching, whatsoever.

Here is a simple example:

using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static async Task<int> TestAsync()
        {
            CallContext.LogicalSetData("valueX", "dataX");
            // commented out on purpose
            // await Task.FromResult(0); 
            Console.WriteLine(CallContext.LogicalGetData("valueX"));
            return 42;
        }

        static void Main(string[] args)
        {
            using(ExecutionContext.SuppressFlow())
            {
                CallContext.LogicalSetData("valueX", "dataXX");
                Console.WriteLine(CallContext.LogicalGetData("valueX"));
                Console.WriteLine(TestAsync().Result);
                Console.WriteLine(CallContext.LogicalGetData("valueX"));
            }
        }
    }
}

It produces this output:

dataXX
dataX
42
dataXX

If I make TestAsync non-async, it works as expected:

static Task<int> TestAsync()
{
    CallContext.LogicalSetData("valueX", "dataX");
    Console.WriteLine(CallContext.LogicalGetData("valueX"));
    return Task.FromResult(42);
}

Output:

dataXX
dataX
42
dataX

I would understand this behavior if I had some real asynchrony inside TestAsync, but that's not the case here. I even use ExecutionContext.SuppressFlow, but that doesn't change anything.

Could someone please explain why it works this way?

avo
  • 10,101
  • 13
  • 53
  • 81

1 Answers1

12

"As expected" in this case is different for different people. :)

In the original Async CTP (which did not modify any framework code), there was no support for an "async-local" kind of context at all. MS modified the LocalCallContext in .NET 4.5 to add this support. The old behavior (with a shared logical context) is especially problematic when working with asynchronous concurrency (i.e., Task.WhenAll).

I explain the high-level mechanics of LocalCallContext within async methods on my blog. The key is here:

When an async method starts, it notifies its logical call context to activate copy-on-write behavior.

There's a special copy-on-write flag in the logical call context that's flipped on whenever an async method starts executing. This is done by the async state machine (specifically, in the current implementation, AsyncMethodBuilderCore.Start invokes ExecutionContext.EstablishCopyOnWriteScope). And "flag" is a simplification - there's no actual boolean member or anything; it just modifies the state (ExecutionContextBelongsToCurrentScope and friends) in a way that any future writes will (shallow) copy the logical call context.

That same state machine method (Start) will call ExecutionContextSwitcher.Undo whenever it is done with the synchronous part of the async method. This is what is restoring the former logical context.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 3
    This is yet another "magic" that adding `async` to a method declaration adds... it's starting to be a bit unpredictable. Perhaps you could write an article on what kind of things adding `async` does nowadays? – Luaan Jul 14 '15 at 16:05
  • 1
    Thank you Stephen. I wonder what else is different for **`async` synchronous** method from regular synchronous method, beside exceptions behavior and this `LocalCallContext` behavior? – avo Jul 14 '15 at 22:50
  • 2
    @avo Off the top of my head.. they can't be inlined unlike regular synchronous methods. – i3arnon Jul 14 '15 at 23:50
  • I believe this behavior is what plagues the HttpContext.Items disappearing after an async call. Similarly, if I use CallContext.LogicalGetData( ) to store an object, changes to that object is reverted after an async call. The only thing that seems to flow properly is AsyncLocal< >. – Brain2000 Aug 06 '18 at 23:29
  • @Brain2000: `HttpContext.Items` should not be affected by `async`. I'd recommend first verifying you have [the correct `targetFramework` set](https://blogs.msdn.microsoft.com/webdev/2012/11/19/all-about-httpruntime-targetframework/) (at least 4.5), and if you are still seeing the problem, you should raise an issue with Microsoft. – Stephen Cleary Aug 07 '18 at 13:58
  • @StephenCleary This is in a custom "IHttpHandler". When using async/await, "HttpContext.Items" is always lost after the await, and I have been searching for an answer for years (perhaps due to the ThreadingContext being null?). AsyncLocal is the first thing that has ever worked. I've read all your articles about ExecutionContext to help me understand what was adding in .NET 4.6. You sir, are a god among men. – Brain2000 Aug 17 '18 at 02:51