Implicit Async Context ("AsyncLocal")• Comments
Occasionally, someone will ask about support for some kind of implicit “context” that will flow with
async code. The most common use case is for logging, where you can maintain a stack of logical operations so every time you log you capture (and log) the state of that stack (e.g., CorrelationManager for
TraceSource, or the Nested Diagnostic Context for log4net). Jon Skeet has a great blog entry on this kind of implicit “context” with several possible uses.
There is a solution for this problem: the “logical call context”, which you can access by CallContext.LogicalGetData and CallContext.LogicalSetData. The regular call context (
CallContext.SetData) acts just like thread-local storage, which of course doesn’t work for
Here’s how logical call context works with asynchronous code.
Logical call context data flows with
ExecutionContext. This means that it’s not affected by
ConfigureAwait(continueOnCapturedContext: false); you can’t “opt-out” of the logical call context. So the logical call context at the beginning of an
async method will always flow through to its continuations.
async method starts, it notifies its logical call context to activate copy-on-write behavior. This means the current logical call context is not actually changed, but it is marked so that if your code does call
CallContext.LogicalSetData, the logical call context data is copied into a new current logical call context before it is changed. Note: the copy-on-write behavior of logical call contexts is only available on .NET 4.5.
This “copying” of the logical call context data is a shallow copy. You can think of the logical call context data as an
IDictionary<string, object> of name/value pairs. When it’s copied, it creates a new dictionary and copies all the name/value pairs into the new dictionary. Both dictionaries then refer to all the same actual object instances; there’s no “deep cloning” of any of your data being done.
Because the references are shared, it’s important not to mutate any values retrieved from the logical call context. If you need to change a logical call context value, update the actual value using
CallContext.LogicalSetData. You should only use immutable types as logical call context data values.
Also note that the design is heavily optimized for the common case: when there is no logical call context data at all. When you start adding “implicit context”, you’re going to start adding overhead. Probably not too much, though, since everything is shallow-copied at worst.
As a final note, remember that you can end up sharing data two different ways: .NET before 4.5 did not have the copy-on-write behavior, and the copies are shallow. For
async code, any sharing of data like this will get you in trouble as soon as you start doing fork/join (e.g.,
Task.WhenAll). So follow the two rules: only .NET 4.5 and immutable data!
Let’s take a simple example. We want to keep an implicit stack of logical operations, and when we call our “log” method we want to output the stack as part of that log.
First, let’s define a strongly-typed accessor for the logical call context data we’ll be using. Remember, we’re only storing immutable data, so we’ll use the new immutable collections:
Updated 2014-06-03: Added the
Wrapper class, which enables code to use
MyStack in cross-AppDomain calls.
So far, so good. Now we have a strongly-typed property we can use to get (or update) the current stack. Next we’ll start defining our public API. We want the ability to “push” a string onto the stack, and get back a disposable that will pop that string back off the stack when disposed. Simple enough:
The final part of our public API is a method that returns the current stack. I’ll just return it as a string:
Now let’s turn our attention to the code that will be using
MyStack. First, our “log” method:
Yeah, that was pretty easy.
Our test code is going to be a bit more complex. First, I’m going to push a “Main” value onto the stack that will last for the entire program. Then, I’ll start two separate (concurrent) pieces of work called “1” and “2”. Each of those are going to log when they start and finish, and they’ll each do some more work called “A” and “B” (sequentially). So we should end up with some interleaving of this output from “1”:
Main 1: <SomeWork> Main 1 A: <MoreWork> Main 1 A: </MoreWork> Main 1 B: <MoreWork> Main 1 B: </MoreWork> Main 1: </SomeWork>
with this output from “2”:
Main 2: <SomeWork> Main 2 A: <MoreWork> Main 2 A: </MoreWork> Main 2 B: <MoreWork> Main 2 B: </MoreWork> Main 2: </SomeWork>
Remember, “1” and “2” are concurrent, so there’s no one right answer for the output. As long as all the messages above are present and in the correct (relative) order, it’s acceptable.
The code, without further ado:
One sample run from my machine is:
Main 1: <SomeWork> Main 1 A: <MoreWork> Main 2: <SomeWork> Main 2 A: <MoreWork> Main 2 A: </MoreWork> Main 1 A: </MoreWork> Main 1 B: <MoreWork> Main 2 B: <MoreWork> Main 2 B: </MoreWork> Main 2: </SomeWork> Main 1 B: </MoreWork> Main 1: </SomeWork>
If you sort out the “1” and the “2” messages, you’ll see that each set is in the correct order and that the stacks are nicely laid out as expected.
Similar code will compile targeting .NET 4.0 (with Microsoft.Bcl.Async); however, it will not work correctly (unless it happens to run on .NET 4.5) because the logical call context is not copied at the right time. In that situation, different parts of different
async methods will end up sharing the same stack (and overwriting each other’s stack).
As a final reminder: this will work only on .NET 4.5 and it only works because we stored immutable data.
Update (2014-12-01): For more details, see Recipe 13.4 in my Concurrency Cookbook.