Reputation: 3928
I've been researching using AsyncLocal
and came across the ThreadContextChanged
property of the AsyncLocalValueChangedArgs<T>
in the change notification. Due to the usual practically non-existent documentation,
I am confused as to what this is for (it is clear from the docs that this is only set when the async value is not changed explicitly, but that's about the only thing that is clear).
There is only one other question here about this (AsyncLocal Value updated to null on ThreadContextChanged) and it refers to a case where the caller is notified of a change with this set to true and the new value set to null, which was unexpected. I'm more concerned with:
My understanding is that AsyncLocal
is used for values that should be carried along with an async execution context, crossing thread boundaries as needed when continuation moves between threads.
I can understand wanting a delegate for when the value is explicitly changed, but the value changing when not explicitly changed (as seems to be what is indicated by this property) seems like nonsense to me.
How can the value change without being explicitly changed? The context can change, but the value within the context shouldn't--that's the whole point of the class.
A context change isn't a value change at all--it's a completely different thing. After all, the class contains multiple values, and the addition of an additional value is a very different operation from the mutation of an existing value, and putting them into the same notification with virtually no explanation to explain the questions above would seem to be deliberately confusing.
Upvotes: 6
Views: 661
Reputation: 3928
I think I've found at least part of the source of the confusion: there are two seemingly different events that cause the notification to happen with ThreadContextChanged set to true. The first is when the call context switches from one thread to another (or has the opportunity to do so). The second is when the call context winds back up the stack. This can be seen when the following code runs:
private static AsyncLocal<string> _localContextId = new AsyncLocal<string>();
private static AsyncLocal<string> _local = new AsyncLocal<string>(c => System.Diagnostics.Debug.WriteLine($"Thread:{System.Threading.Thread.CurrentThread.ManagedThreadId},Context:{_localContextId.Value ?? Guid.Empty.ToString("N")},Previous:{c.PreviousValue ?? "<null>"},Current:{c.CurrentValue ?? "<null>"},ThreadContextChanged:{c.ThreadContextChanged}"));
[TestMethod]
public async Task AsyncLocalTest1()
{
_localContextId.Value = Guid.NewGuid().ToString("N");
DumpState(1, 0);
await Task.Delay(100);
DumpState(1, 1);
_local.Value = "AsyncLocalTest1";
DumpState(1, 2);
await Task.Delay(100);
DumpState(1, 3);
await AsyncLocalTest2();
DumpState(1, 4);
await Task.Delay(100);
DumpState(1, 5);
}
private async Task AsyncLocalTest2()
{
DumpState(2, 0);
await Task.Delay(100);
DumpState(2, 1);
_local.Value = "AsyncLocalTest2";
DumpState(2, 2);
await Task.Delay(100);
DumpState(2, 3);
await AsyncLocalTest3();
DumpState(2, 4);
await Task.Delay(100);
DumpState(2, 5);
}
private async Task AsyncLocalTest3()
{
DumpState(3, 0);
await Task.Delay(100);
DumpState(3, 1);
_local.Value = "AsyncLocalTest3";
DumpState(3, 2);
await Task.Delay(100);
DumpState(3, 3);
}
private void DumpState(int major, int minor)
{
System.Diagnostics.Debug.WriteLine($"Thread:{System.Threading.Thread.CurrentThread.ManagedThreadId},AsyncLocalTest{major}.{minor}: " + _local.Value ?? "<null>");
}
and produces the following output (this is just a sample--under load there would likely be more threads involved):
Thread:13,AsyncLocalTest1.0:
Thread:9,AsyncLocalTest1.1:
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest1,ThreadContextChanged:False
Thread:9,AsyncLocalTest1.2: AsyncLocalTest1
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest1,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest1,ThreadContextChanged:True
Thread:9,AsyncLocalTest1.3: AsyncLocalTest1
Thread:9,AsyncLocalTest2.0: AsyncLocalTest1
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest1,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest1,ThreadContextChanged:True
Thread:9,AsyncLocalTest2.1: AsyncLocalTest1
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest1,Current:AsyncLocalTest2,ThreadContextChanged:False
Thread:9,AsyncLocalTest2.2: AsyncLocalTest2
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest2,Current:AsyncLocalTest1,ThreadContextChanged:True
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest1,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,AsyncLocalTest2.3: AsyncLocalTest2
Thread:9,AsyncLocalTest3.0: AsyncLocalTest2
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest2,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,AsyncLocalTest3.1: AsyncLocalTest2
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest2,Current:AsyncLocalTest3,ThreadContextChanged:False
Thread:9,AsyncLocalTest3.2: AsyncLocalTest3
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest3,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest2,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest3,ThreadContextChanged:True
Thread:9,AsyncLocalTest3.3: AsyncLocalTest3
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest3,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,AsyncLocalTest2.4: AsyncLocalTest2
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest2,Current:AsyncLocalTest3,ThreadContextChanged:True
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest3,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,AsyncLocalTest2.5: AsyncLocalTest2
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest2,Current:AsyncLocalTest1,ThreadContextChanged:True
Thread:9,AsyncLocalTest1.4: AsyncLocalTest1
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:AsyncLocalTest1,Current:AsyncLocalTest2,ThreadContextChanged:True
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest2,Current:<null>,ThreadContextChanged:True
Thread:9,Context:002d421118804fb3b6f40941df50e056,Previous:<null>,Current:AsyncLocalTest1,ThreadContextChanged:True
Thread:9,AsyncLocalTest1.5: AsyncLocalTest1
Thread:9,Context:00000000000000000000000000000000,Previous:AsyncLocalTest1,Current:<null>,ThreadContextChanged:True
It appears that two calls happen every time the call context is interrupted, first changing the value to null and then changing it back (which could happen on another thread, but not necessarily). I would presume that the first is when the call context and the thread are disassociated and the seconds is when they are reassociated. When the stack unwinds, there is an additional call that changes the value for that.
Upvotes: 1
Reputation: 81513
You have a lot of partial questions and musings. However, let's take a step back and look at the fundamentals.
why this property exists
It simply allows you to determine what changed the value. **anti climax**
under what conditions you get a notification with this set, and
IAsyncLocal.OnValueChanged
gets called with updated values as you go deeper into the await stack and change the value. Then it rewinds as you await back out of the stack, giving you previous values.
ThreadContextChanged
is merely just letting you know if the value changed because it's switched to a different ExecutionContext
or someone set the Value
property.
what call and/or thread context is active when you receive this notification.
Let's have a look at the source:
public T Value
{
...
set
{
ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);
}
}
ExecutionContext.cs
internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
{
...
if (needChangeNotifications)
{
if (hadPreviousValue)
Contract.Assert(current.m_localChangeNotifications.Contains(local));
else
current.m_localChangeNotifications.Add(local);
local.OnValueChanged(previousValue, newValue, false);
}
}
After the context switch:
private static void OnContextChanged(ExecutionContext previous, ExecutionContext current)
{
previous = previous ?? Default;
foreach (IAsyncLocal local in previous.m_localChangeNotifications)
{
object previousValue;
object currentValue;
previous.m_localValues.TryGetValue(local, out previousValue);
current.m_localValues.TryGetValue(local, out currentValue);
if (previousValue != currentValue)
local.OnValueChanged(previousValue, currentValue, true);
}
As you can see the event is raised in the context of where the value changed, which may include the new context or queued up after the change.
How is this useful? Well, personally I haven't used it myself, and is likely implemented in some dark corner of the framework to solve a specific problem, but I guess it might help in situations where you are trying to minimize locking and synchronization.
I have never seen this used, and as you have noted it's very difficult to find concrete implementations in actual code. However nothing is stopping you writing a few tests.
Upvotes: 2