Depending on whether I'm using async/await based code or TPL based code, I'm getting two different behaviors regarding the clean-up of logical CallContext
.
I can set and clear logical CallContext
exactly as I expect if I use the following async/await code:
class Program
{
static async Task DoSomething()
{
CallContext.LogicalSetData("hello", "world");
await Task.Run(() =>
Debug.WriteLine(new
{
Place = "Task.Run",
Id = Thread.CurrentThread.ManagedThreadId,
Msg = CallContext.LogicalGetData("hello")
}))
.ContinueWith((t) =>
CallContext.FreeNamedDataSlot("hello")
);
return;
}
static void Main(string[] args)
{
DoSomething().Wait();
Debug.WriteLine(new
{
Place = "Main",
Id = Thread.CurrentThread.ManagedThreadId,
Msg = CallContext.LogicalGetData("hello")
});
}
}
The above outputs the following:
{ Place = Task.Run, Id = 9, Msg = world }
{ Place = Main, Id = 8, Msg = }
Notice the Msg =
which indicates that CallContext
on the main thread has been freed and is empty.
But when I switch to pure TPL / TAP code I can't achieve the same effect...
class Program
{
static Task DoSomething()
{
CallContext.LogicalSetData("hello", "world");
var result = Task.Run(() =>
Debug.WriteLine(new
{
Place = "Task.Run",
Id = Thread.CurrentThread.ManagedThreadId,
Msg = CallContext.LogicalGetData("hello")
}))
.ContinueWith((t) =>
CallContext.FreeNamedDataSlot("hello")
);
return result;
}
static void Main(string[] args)
{
DoSomething().Wait();
Debug.WriteLine(new
{
Place = "Main",
Id = Thread.CurrentThread.ManagedThreadId,
Msg = CallContext.LogicalGetData("hello")
});
}
}
The above outputs the following:
{ Place = Task.Run, Id = 10, Msg = world }
{ Place = Main, Id = 9, Msg = world }
Is there anything I can do to coerce TPL to "free" the logical CallContext
the same way as the async/await code does?
I am not interested in alternatives to CallContext
.
I'm hoping to get the above TPL/TAP code fixed so that I can use it in projects targeting the .net 4.0 framework. If that is not possible in .net 4.0, I'm still curious if it can be done in .net 4.5.
In an
async
method theCallContext
is copied on write:From Implicit Async Context ("AsyncLocal")
That means that in your
async
version theCallContext.FreeNamedDataSlot("hello")
continuation is redundant as even without it:The
CallContext
inMain
wouldn't contain the"hello"
slot:In the TPL equivalent all code outside the
Task.Run
(which should beTask.Factory.StartNew
asTask.Run
was added in .Net 4.5) runs on the same thread with the same exactCallContext
. If you want to clean it you need to do that on that context (and not in the continuation):You can even abstract a scope out of it to make sure you always clean up after yourself:
Using:
A good question. The
await
version may not work the way you may think it does here. Let's add another logging line insideDoSomething
:Output:
Note the
"world"
is still there afterawait
, because it was there beforeawait
. And it is not there afterDoSomething().Wait()
because it wasn't there before it, in the first place.Interestingly enough, the
async
version ofDoSomething
creates a copy-on-write clone of theLogicalCallContext
for its scope, upon the firstLogicalSetData
. It does that even when there is no asynchrony inside it - tryawait Task.FromResult(0)
. I presume the wholeExecutionContext
gets cloned for the scope of theasync
method, upon the 1st write operation.OTOH, for the non-async version there is no "logical" scope and no outer
ExecutionContext
here, so the copy-on-write clone ofExecutionContext
becomes current for theMain
thread (but the continuations and theTask.Run
lambdas still get their own clones). So, you'd either need to moveCallContext.LogicalSetData("hello", "world")
inside theTask.Run
lambda, or clone the context manually: