This article explains how
AsyncLocal values are stored in .NET and how to retrieve them from a memory dump. Note that the code provided is written for .NET 5, but should be able to work for .NET Framework with minor modifications (the name of some internal fields have changed across versions).
Preparing the memory dump
First thing first, we need to prepare the memory dump that will serve as example for the whole article. For that, I used the following code:
The program creates an array of
AsyncLocal<int> of 17 elements. 17 is not a random number, its significance will become clear in a moment. Then the program creates a few threads, and have them store a different number of asynclocal values, such as thread 1 will store 1 asynclocal value, thread 2 will store 2, and so on until thread 17. When all the values are initialized, the program displays a prompt and pauses to give time to capture a memory dump. This can be done using the procdump tool:
procdump -ma <pid>
Inspecting the memory dump with WinDbg
Now that we have our memory dump, let’s start by inspecting it with WinDbg. We’ll start by retrieving the array of
AsyncLocal<int>. Recent versions of WinDbg automatically load the SOS extension when dealing with .NET Core memory dumps, so that’s one less thing to do. We can use the
dumpheap command to locate our instance of
Program, and from there access the array.
For that, we first use
dumpheap -stat -type ClientApp.Programto get the address of the MT, and then we feed it to
dumpheap -mt to get the address of the instance:
Then we use
dumpobj to dump the instance and from there we get the address of the value stored in the
Now we can dump the contents of the array using
dumparray and inspect the individual items:
Immediately we can see what the issue is going to be: in our instance of
AsyncLocal there is… nothing. Literally nothing at all is stored inside, unless you provided a
valueChanged handler (in which case, you will only find the address of your handler).
So where are those values stored? On the
Thread object itself. The instance of
AsyncLocal is just used as a key to retrieve the value from the execution context. If we take one thread at random, we can find all the asynclocal values from that thread stored in the
m_LocalValues field of the execution context:
_key1 field contains the address of the
AsyncLocal instance, and the
_value1 field contains the value. So to check all the values stores in an
AsyncLocal instance, we need to inspect every single thread. Not only that, but the way the values are stored is going to change depending on how many different
AsyncLocal values are associated to a given thread. Managing that in WinDbg is going to be very tricky, so it’s time to switch to ClrMD.
Inspecting the memory dump with ClrMD
Our ClrMD program will open the memory dump, find the
AsyncLocal array, and extract all the values for every thread. To make things easier, we’re going to use the DynaMD library.
First thing first, we open the memory dump, and we locate the first (and only) instance of
AsyncLocal<int> thanks to the
GetProxies extension method. Then we iterate on every instance and give them to a helper method that will extract a list of thread ids and their associated values:
The helper method will iterate on every thread, inspect the
AsyncLocal values stored in the execution context, and filter them to only return the one associated to the given instance:
Now we just need to extract the values from the
AsyncLocal storage… Which is where things get fancy. It turns out there’s not one but many different storage implementations, depending on how many values the current thread is storing.
From 1 to 3 values
If a given thread stores one to three different
AsyncLocal values, it will use an instance of
ThreeElementAsyncLocalValueMap. Those types just have a list of fields to store the keys and the values,
_value3 respectively. Extracting the values is just a matter of reading those fields:
From 4 to 16 values
If a given thread stores four to sixteen values, it will thankfully stop declaring a field for every value and instead use an array-backed storage:
MultiElementAsyncLocalValueMap. All the keys and values are stored in the
_keyValues field as an array of
KeyValuePair<IAsyncLocal, object>. The array is allocated with exactly the right size, so we just need to iterate on it to return the contents. It’s very straightforward with DynaMD:
More than 16 values
To read from an
AsyncLocal storage, the runtime has to find the value associated to a given key. Iterating on an array becomes less and less efficient as the number of values increases, so the runtime fallbacks on a
ManyElementAsyncLocalValueMap when that number gets above 16. This type directly inherits from
Dictionary<IAsyncLocal, object>, so to extract the values we need to iterate on the
Putting everything together
At this point, you may be wondering how everything fits together. Every time you add or remove a value from an
AsyncLocal instance, the runtime will allocate a new storage based on the new number of elements, and copy everything from the old to the new storage. This makes adding/removing values very inefficient, so use
AsyncLocal with care.
In any case, we now have helper methods for every possible
AsyncLocal storage, so we can just call the appropriate one in our
And that’s it! Now we can run the program and inspect the values for every
The full code is available here.