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?
"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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With