这是今天帮柠檬分析一个AsyncLocal相关的问题时发现的.
试想这个代码输出的值是多少?git
using System; using System.Threading; using System.Threading.Tasks; namespace asynclocal { class Program { public static AsyncLocal<int> v = new AsyncLocal<int>(); static void Main(string[] args) { var task = Task.Run(async () => { v.Value = 123; var intercept = new Intercept(); await Intercept.Invoke(); Console.WriteLine(Program.v.Value); }); task.Wait(); } } public class Intercept { public static async Task Invoke() { Program.v.Value = 888; } } }
答案是123.
为何修改了AsyncLocal
的值却无效呢?github
这要从AsyncLocal的运做机制提及.
首先这是AsyncLocal的源代码:异步
public T Value { get { object obj = ExecutionContext.GetLocalValue(this); return (obj == null) ? default(T) : (T)obj; } set { ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null); } }
获取和设置值用的是ExecutionContext.GetLocalValue
和ExecutionContext.SetLocalValue
这两个静态函数.
这两个静态函数的源代码在ExecutionContext中:async
internal static object GetLocalValue(IAsyncLocal local) { ExecutionContext current = Thread.CurrentThread.ExecutionContext; if (current == null) return null; object value; current.m_localValues.TryGetValue(local, out value); return value; } internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications) { ExecutionContext current = Thread.CurrentThread.ExecutionContext ?? ExecutionContext.Default; object previousValue; bool hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue); if (previousValue == newValue) return; IAsyncLocalValueMap newValues = current.m_localValues.Set(local, newValue); // // Either copy the change notification array, or create a new one, depending on whether we need to add a new item. // IAsyncLocal[] newChangeNotifications = current.m_localChangeNotifications; if (needChangeNotifications) { if (hadPreviousValue) { Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0); } else { int newNotificationIndex = newChangeNotifications.Length; Array.Resize(ref newChangeNotifications, newNotificationIndex + 1); newChangeNotifications[newNotificationIndex] = local; } } Thread.CurrentThread.ExecutionContext = new ExecutionContext(newValues, newChangeNotifications, current.m_isFlowSuppressed); if (needChangeNotifications) { local.OnValueChanged(previousValue, newValue, false); } }
看到SetLocalValue
里面的处理了吗? 每一次修改值之后都会生成一个新的执行上下文而后覆盖到当前的线程对象上.ide
咱们再来看看调用一个异步函数时的代码:函数
// Token: 0x06000004 RID: 4 RVA: 0x000020B0 File Offset: 0x000002B0 .method public hidebysig static class [System.Runtime]System.Threading.Tasks.Task Invoke () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = ( 01 00 21 61 73 79 6e 63 6c 6f 63 61 6c 2e 49 6e 74 65 72 63 65 70 74 2b 3c 49 6e 76 6f 6b 65 3e 64 5f 5f 30 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) // Header Size: 12 bytes // Code Size: 52 (0x34) bytes // LocalVarSig Token: 0x11000002 RID: 2 .maxstack 2 .locals init ( [0] class asynclocal.Intercept/'<Invoke>d__0', [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder ) /* 0x000002BC 7309000006 */ IL_0000: newobj instance void asynclocal.Intercept/'<Invoke>d__0'::.ctor() /* 0x000002C1 0A */ IL_0005: stloc.0 /* 0x000002C2 06 */ IL_0006: ldloc.0 /* 0x000002C3 281700000A */ IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create() /* 0x000002C8 7D05000004 */ IL_000C: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder' /* 0x000002CD 06 */ IL_0011: ldloc.0 /* 0x000002CE 15 */ IL_0012: ldc.i4.m1 /* 0x000002CF 7D04000004 */ IL_0013: stfld int32 asynclocal.Intercept/'<Invoke>d__0'::'<>1__state' /* 0x000002D4 06 */ IL_0018: ldloc.0 /* 0x000002D5 7B05000004 */ IL_0019: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder' /* 0x000002DA 0B */ IL_001E: stloc.1 /* 0x000002DB 1201 */ IL_001F: ldloca.s 1 /* 0x000002DD 1200 */ IL_0021: ldloca.s 0 /* 0x000002DF 280100002B */ IL_0023: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class asynclocal.Intercept/'<Invoke>d__0'>(!!0&) /* 0x000002E4 06 */ IL_0028: ldloc.0 /* 0x000002E5 7C05000004 */ IL_0029: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder' /* 0x000002EA 281900000A */ IL_002E: call instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task() /* 0x000002EF 2A */ IL_0033: ret } // end of method Intercept::Invoke
异步函数会编译成一个状态机(类型)而后经过System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start
执行,
System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start的源代码以下:ui
/// <summary>Initiates the builder's execution with the associated state machine.</summary> /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam> /// <param name="stateMachine">The state machine instance, passed by reference.</param> [DebuggerStepThrough] public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { if (stateMachine == null) // TStateMachines are generally non-nullable value types, so this check will be elided { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); } // Run the MoveNext method within a copy-on-write ExecutionContext scope. // This allows us to undo any ExecutionContext changes made in MoveNext, // so that they won't "leak" out of the first await. Thread currentThread = Thread.CurrentThread; ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher); try { ExecutionContext.EstablishCopyOnWriteScope(currentThread, ref ecs); stateMachine.MoveNext(); } finally { ecs.Undo(currentThread); } }
执行状态机前会调用ExecutionContext.EstablishCopyOnWriteScope, 源代码以下:this
internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw) { Debug.Assert(currentThread == Thread.CurrentThread); ecsw.m_ec = currentThread.ExecutionContext; ecsw.m_sc = currentThread.SynchronizationContext; }
执行状态机后会调用ExecutionContextSwitcher::Undo, 源代码以下:spa
internal void Undo(Thread currentThread) { Debug.Assert(currentThread == Thread.CurrentThread); // The common case is that these have not changed, so avoid the cost of a write if not needed. if (currentThread.SynchronizationContext != m_sc) { currentThread.SynchronizationContext = m_sc; } if (currentThread.ExecutionContext != m_ec) { ExecutionContext.Restore(currentThread, m_ec); } }
总结起来:线程
ExecutionContext
并覆盖到Thread.CurrentThread.ExecutionContext
Thread.CurrentThread.ExecutionContext
Thread.CurrentThread.ExecutionContext
再来看看文章开头我给出的代码中的处理流程:
{ }
{ <int>: 123 }
{ <int>: 123 }
{ <int>: 888 }
{ <int>: 123 }
到这里就很清楚了.
await外的AsyncLocal值能够传递到await内, await内的AsyncLocal值没法传递到await外(只能读取不能修改).
这个问题在StackOverflow上有人提过, 但回应不多.
微软是故意这样设计的, 不然就没法实现MSDN上的这个例子了.
但我我的认为这是个设计错误, 柠檬她给出的例子本意是想在aop拦截器中覆盖AsyncLocal中的Http上下文, 但明显这样作是行不通的. 我建议编写csharp代码时尽量的不要使用ThreadLocal和AsyncLocal.