這是今天幫 "檸檬" 分析一個 "AsyncLocal相關的問題" 時發現的. 試想這個代碼輸出的值是多少? 答案是123. 為什麼修改了 的值卻無效呢? 這要從AsyncLocal的運作機制說起. 首先這是 "AsyncLocal的源代碼" : 獲取和設置值用的是 和`ExecutionConte ...
這是今天幫檸檬分析一個AsyncLocal相關的問題時發現的.
試想這個代碼輸出的值是多少?
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
的值卻無效呢?
這要從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中:
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
裡面的處理了嗎? 每一次修改值以後都會生成一個新的執行上下文然後覆蓋到當前的線程對象上.
我們再來看看調用一個非同步函數時的代碼:
// 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的源代碼如下:
/// <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, 源代碼如下:
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, 源代碼如下:
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);
}
}
總結起來:
- AsyncLocal每設置一次值就會創建一個新的
ExecutionContext
並覆蓋到Thread.CurrentThread.ExecutionContext
- 執行狀態機前會備份當前的
Thread.CurrentThread.ExecutionContext
- 執行狀態機後會恢復備份的
Thread.CurrentThread.ExecutionContext
再來看看文章開頭我給出的代碼中的處理流程:
- 初始的執行上下文為空, 且叫
{ }
- 修改AsyncLocal的值到123後, 執行上下文變為
{ <int>: 123 }
- 調用Intercept.Invoke前備份了執行上下文, 備份的是
{ <int>: 123 }
- Intercept.Invoke修改AsyncLocal的值到888後, 執行上下文變為
{ <int>: 888 }
- 調用Intercept.Invoke後恢復備份的上下文, 恢復後是
{ <int>: 123 }
到這裡就很清楚了.
await外的AsyncLocal值可以傳遞到await內, await內的AsyncLocal值無法傳遞到await外(只能讀取不能修改).
這個問題在StackOverflow上有人提過, 但回應很少.
微軟是故意這樣設計的, 否則就無法實現MSDN上的這個例子了.
但我個人認為這是個設計錯誤, 檸檬她給出的例子本意是想在aop攔截器中覆蓋AsyncLocal中的Http上下文, 但明顯這樣做是行不通的.
我建議編寫csharp代碼時儘可能的不要使用ThreadLocal和AsyncLocal.