In this question the accepted answer by Stephen Cleary says that LogicalCallContext can\'t work correctly with async. He also posted about it in this MSDN thread.
Logica
Update: This answer is not correct for .NET 4.5. See my blog post on AsyncLocal for details.
Here's the situation (repeating several points in your question):
LogicalCallContext
will flow with async
calls; you can use it to set some implicit data and read it from an async
method further down your call stack.LogicalCallContext
are shallow copies, without any way for end-user code to hook into a deep-copy kind of operation.async
, there is only one copy of the LogicalCallContext
shared between the various async
methods.LogicalCallContext
does work fine if your async
code is all linear:
async Task ParentAsync()
{
... = 0; // Set LogicalCallContext value to "0".
await ChildAAsync();
// LogicalCallContext value here is always "0".
await ChildBAsync();
// LogicalCallContext value here is always "0".
}
async Task ChildAAsync()
{
int value = ...; // Save LogicalCallContext value (always "0").
... = 1; // Set LogicalCallContext value to "1".
await Task.Delay(1000);
// LogicalCallContext value here is always "1".
... = value; // Restore original LogicalCallContext value (always "0").
}
async Task ChildBAsync()
{
int value = ...; // Save LogicalCallContext value (always "0").
... = 2; // Set LogicalCallContext value to "2".
await Task.Delay(1000);
// LogicalCallContext value here is always "2".
... = value; // Restore original LogicalCallContext value (always "0").
}
But things aren't so nice once you use what I call "simple parallelism" (starting several async
methods and then using Task.WaitAll
or similar). This is an example similar to my MSDN forum post (for simplicity, assume a non-parallel SynchronizationContext
such as GUI or ASP.NET):
Edit: the code comments are incorrect; see the comments on this question and answer
async Task ParentAsync()
{
... = 0; // Set LogicalCallContext value to "0".
Task childTaskA = ChildAAsync();
// LogicalCallContext value here is always "1".
Task childTaskB = ChildBAsync();
// LogicalCallContext value here is always "2".
await Task.WhenAll(childTaskA, childTaskB);
// LogicalCallContext value here may be "0" or "1".
}
async Task ChildAAsync()
{
int value = ...; // Save LogicalCallContext value (always "0").
... = 1; // Set LogicalCallContext value to "1".
await Task.Delay(1000);
// LogicalCallContext value here may be "1" or "2".
... = value; // Restore original LogicalCallContext value (always "0").
}
async Task ChildBAsync()
{
int value = ...; // Save LogicalCallContext value (always "1").
... = 2; // Set LogicalCallContext value to "2".
await Task.Delay(1000);
// LogicalCallContext value here may be "0" or "2".
... = value; // Restore original LogicalCallContext value (always "1").
}
The problem is that the LogicalCallContext
is shared between ParentAsync
, ChildAAsync
, and ChildBAsync
, without any way to hook into or force a deep-copy operation. In the "linear" example, the context is also shared, but only one method was active at a time.
Even if the data you store in LogicalCallContext
is immutable (as in my integer example), you'd still have to update the LogicalCallContext
value in order to implement an NDC, and this means the sharing-without-copies problem is going to mess it up.
I have looked into this in detail, and have concluded that a solution is not possible. If you can figure one out, I'd be very happy to be proven wrong. :)
P.S. Stephen Toub pointed out that the recommendation to use CallContext
only for remoting (which was given without reason, IIRC) no longer applies. We may feel free to use LogicalCallContext
... if we can get it to work. ;)
Stephen confirms that this works on .Net 4.5 and Win8/2012. Not tested on other platforms, and known not to work on at least some of them. So the answer is that Microsoft got their game together and fixed the underlying issue in at least the most recent version of .Net and the async compiler.
So the answer is, it does work, just not on older .Net versions. (So the log4net project can't use it to provide a generic NDC.)