奇怪的事情就发生了,为什么明明在Method中把Context对象置为null了, Method-3 中已经输出为null了,为啥在 Main-2 输出中还是ContextValue呢?
那为什么会造成这个问题呢?首先我们得知道 AsyncLocal 是如何实现的,这里我就不在赘述,详细可以看我前面给的链接(黑洞大佬的文章)。这里只简单的说一下,我们只需要知道 AsyncLocal 底层是通过 ExecutionContext 实现的,每次设置Value时都会用新的Context对象来覆盖原有的,代码如下所示(有删减)。
......
public
sealed
class
ExecutionContext
:
IDisposable
,
ISerializable
{
internal
static void SetLocalValue(IAsyncLocal local,
object
newValue, bool needChangeNotifications)
{
var
current = Thread.CurrentThread.GetMutableExecutionContext;
object
previousValue =
null
;
if
(previousValue == newValue)
return
;
var
newValues = current._localValues;
// 无论是AsyncLocalValueMap.Create 还是 newValues.Set
// 都会创建一个新的IAsyncLocalValueMap对象来覆盖原来的值
if
(newValues ==
null
)
{
newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
}
else
{
newValues = newValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
}
current._localValues = newValues;
......
}
}
接下来我们需要避开 await/async 语法糖的影响,反编译一下IL代码,使用C# 1.0来重新组织代码(使用ilspy或者dnspy之类都可以)。
可以看到原本的语法糖已经被拆解成stackless状态机,这里我们重点关注 Start 方法。进入 Start 方法内部,我们可以看到以下代码,源码链接。
......
// Start方法
public
static
void
Start<TStateMachine>(
ref
TStateMachine stateMachine)
where
TStateMachine : IAsyncStateMachine
{
if
(stateMachine ==
null
)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
Thread currentThread = Thread.CurrentThread;
// 备份当前线程的 executionContext
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
// 执行状态机
stateMachine.MoveNext;
}
finally
{
if
(previousSyncCtx != currentThread._synchronizationContext)
{
// Restore changed SynchronizationContext back to previous
currentThread._synchronizationContext = previousSyncCtx;
}
ExecutionContext? currentExecutionCtx = currentThread._executionContext;
// 如果executionContext发生变化,那么调用RestoreChangedContextToThread方法还原
if
(previousExecutionCtx != currentExecutionCtx)
{
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentExecutionCtx);
}
}
}
......
// 调用RestoreChangedContextToThread方法
internal
static
void
RestoreChangedContextToThread
(
Thread currentThread, ExecutionContext? contextToRestore, ExecutionContext? currentContext
)
{
Debug.Assert(currentThread == Thread.CurrentThread);
Debug.Assert(contextToRestore != currentContext);
// 将改变后的ExecutionContext恢复到之前的状态
currentThread._executionContext = contextToRestore;
......
}
通过上面的代码我们就不难看出,为什么会存在这样的问题了,是因为状态机的 Start 方法会备份当前线程的 ExecuteContext ,如果 ExecuteContext 在状态机内方法调用时发生了改变,那么就会
还原
回去。
又因为上文提到的 AsyncLocal 底层实现是 ExecuteContext ,每次SetValue时都会生成一个新的 IAsyncLocalValueMap 对象覆盖当前的 ExecuteContext ,必然修改就会被
还原
回去了。
ASP.NET Core的解决方案
在ASP.NET Core中,解决这个问题的方法也很巧妙,就是简单的包了一层。我们也可以简单的包一层对象。
public
class
ContextHolder
{
public
string
Context {
get
;
set
;}
}
public
class
ContextAccessor
{
static
AsyncLocal<ContextHolder> _contextCurrent =
new
AsyncLocal<ContextHolder>;
public
string
Context
{
get
=> _contextCurrent.Value?.Context;
set
{
var
holder = _contextCurrent.Value;
// 拿到原来的holder 直接修改成新的value
// asp.net core源码是设置为null 因为在它的逻辑中执行到了这个Set方法
// 就必然是一个新的http请求,需要把以前的清空
if
(holder !=
null
) holder.Context =
value
;
// 如果没有holder 那么新建
else
_contextCurrent.Value =
new
ContextHolder { Context =
value
};
}
}
}
最终结果就和我们预期的一致了,流程也如下图一样。自始至终都是修改的同一个 ContextHolder 对象。
总结
由上可见,ASP.NET Core 6.0的 HttpContextAccessor 那样设计的原因就是为了解决AsyncLocal在 await 环境中会发生复制,导致不能及时清除历史的 HttpContext 的问题。
笔者水平有限,如果错漏,欢迎指出,感谢各位的阅读!
作者:InCerry
出处:https://www.cnblogs.com/InCerry/p/Why-The-Design-HttpContextAccessor.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:本博客版权归「InCerry」所有。
返回搜狐,查看更多