这是今天帮柠檬分析一个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.GetLocalValueExecutionContext.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.

AsyncLocal的运作机制和陷阱的更多相关文章

  1. Hadoop-Yarn-框架原理及运作机制(原理篇)

    文件为转载:http://blog.csdn.net/liuwenbo0920/article/details/43304243 一.YARN基本架构 YARN是Hadoop 2.0中的资源管理系统, ...

  2. Hadoop-Yarn-框架原理及运作机制

    一.YARN基本架构 YARN是Hadoop 2.0中的资源管理系统,它的基本设计思想是将MRv1中的JobTracker拆分成了两个独立的服务:一个全局的资源管理器ResourceManager和每 ...

  3. Hadoop Yarn 框架原理及运作机制及与MapReduce比较

    Hadoop 和 MRv1 简单介绍 Hadoop 集群可从单一节点(其中所有 Hadoop 实体都在同一个节点上运行)扩展到数千个节点(其中的功能分散在各个节点之间,以增加并行处理活动).图 1 演 ...

  4. redis底层设计(五)——内部运作机制

    5.1 数据库 5.1.1 数据库的结构: Redis 中的每个数据库,都由一个redis.h/redisDb 结构表示: typedef struct redisDb { // 保存着数据库以整数表 ...

  5. Redis的内部运作机制

    本文将分五个部分来分析和总结Redis的内部机制,分别是:Redis数据库.Redis客户端.Redis事件.Redis服务器的初始化步骤.Redis命令的执行过程. 首先介绍一下Redis服务器的状 ...

  6. Redis的结构和运作机制

    目录 1.数据库的结构 1.1 字典的底层实现 2.过期键的检查和清除 2.1 定时删除 2.2 惰性删除 2.3 定期删除 2.4 对RDB.AOF和复制的影响 3.持久化机制 3.1 RDB方式 ...

  7. 【SpringCloud技术专题】「Eureka源码分析」从源码层面让你认识Eureka工作流程和运作机制(上)

    前言介绍 了解到了SpringCloud,大家都应该知道注册中心,而对于我们从过去到现在,SpringCloud中用的最多的注册中心就是Eureka了,所以深入Eureka的原理和源码,接下来我们要进 ...

  8. servlet运作机制

    最近研究zipkin,在研究客户端brave的时候,才算开始理解servlet了.    servlet只是tomcat被实例化一次:    之后每次访问其实都是对同一个servlet示例操作:所以, ...

  9. 透过字节码生成审视Java动态代理运作机制

    对于动态代理我想应该大家都不陌生,就是可以动态去代理实现某个接口的类来干一些我们自己想要的功能,但是在字节码层面它的表现是如何的呢?既然目前刚好在研究字节码相关的东东,有必要对其从字节码角度来审视一下 ...

随机推荐

  1. 基础教程:ASP.NET Core 2.0 MVC筛选器

    问题 如何在ASP.NET Core的MVC请求管道之前和之后运行代码. 解 在一个空的项目中,更新 Startup 类以添加MVC的服务和中间件. publicvoid ConfigureServi ...

  2. 聊下 git 多账户问题

    git 多账户问题 标签(空格分隔):git github gitlab git多账户 背景 git 多账号配置 ssh 多密钥对配置 背景 在使用 git 的时候我们都会面临多账户问题,比较常见的就 ...

  3. 重构手法之Split Temporary Variable(分解临时变量)

    返回总目录 本小节目录 Split Temporary Variable(分解临时变量) Remove Assignments to Parameters(移除对参数的赋值) 6.6Split Tem ...

  4. IDEA热部署(二)---jetty插件启动maven项目

    jetty插件的配置 我们使用jetty插件来进行启动我们的maven项目,在pom.xml中进行配置: <plugins> <plugin> <groupId>o ...

  5. CSharpGL(47)你好,Framebuffer!

    CSharpGL(47)你好,Framebuffer! Framebuffer对象(FBO)是一种复杂的OpenGL对象.使用自定义的framebuffer,可以实现离屏渲染,进而实现很多高级功能,例 ...

  6. 实现基于Keepalived主从高可用集群网站架构

    背景 上一期我们实现了基于lvs负载均衡集群的电商网站架构,随着业务的发展,网站的访问量越来越大,网站访问量已经从原来的1000QPS,变为3000QPS,目前业务已经通过集群LVS架构可做到随时拓展 ...

  7. ionic开发遇到的坑及总结

    前言 ionic是一个用来开发混合手机应用的,开源的,免费的代码库.可以优化html.css和js的性能,构建高效的应用程序,而且还可以用于构建Sass和AngularJS的优化.ionic会是一个可 ...

  8. JIT——即时编译的原理

     介绍 java 作为静态语言十分特殊,他需要编译,但并不是在执行之前就编译为本地机器码. 所以,在谈到 java的编译机制的时候,其实应该按时期,分为两个部分.一个是 javac指令 将java源码 ...

  9. null与undefined的比较

    null在JavaScript中是关键字,它属于一个特殊的值,即空值. 而undefined它不是关键字,它表示未定义,属于预定义的全局变量. null == undefined 返回的是 true  ...

  10. xampp 出现403 无法访问问题(已解决)

    最近重新安装xampp,配置虚拟主机做本地测试,但是总是出现服务器无法访问,权限不够的提示. 查找error文件后排查错误,发现是权限的问题.具体错误如下: 重新查看配置文件httpd.conf,才发 ...