这是今天帮柠檬分析一个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. 深入理解CSS盒模型

    如果你在面试的时候面试官让你谈谈对盒模型的理解,你是不是不知从何谈起.这种看似简单的题其实是最不好答的. 下面本文章将会从以下几个方面谈谈盒模型. 基本概念:标准模型 和IE模型 CSS如何设置这两种 ...

  2. pt-show-grants

    用法: pt-show-grants [OPTION ... ] [DSN]   例子: pt-show-grants pt-show-grants --separate --revoke | dif ...

  3. 微信JS-SDK 选取手机照片并进行上传

    项目中遇到需要选取照片上传的需求,因为网页运行在微信的浏览器里面,所以用微信的 js-sdk 提供的选取照片功能,来进行项目开发.实际开发中需要用到微信web开发者工具,详细参考链接:https:// ...

  4. spring各个版本开发包下载

    spring各个开发包版本下载地址:https://repo.spring.io/webapp/#/artifacts/browse/tree/General/libs-release-local/o ...

  5. 《HelloGitHub》第 20 期

    前言 HelloGitHub 项目已经累积到 3k+ Stars.本项能够走到今天,帮助到越来越多的人.少不了热爱开源.不断为本项目贡献项目的小伙伴们. 贡献者列表 很多人都有想法,付诸于行动在少数, ...

  6. 二、Hadoop学习笔记————架构学习

    1.成百上千台服务器组成集群,需要时刻检测服务器是否故障 2.用流读取数据更加高效快速 3.存储节点具有运算功能,省略了服务器之间来回传数据的网络带宽限制 4.一次写入,多次访问,不修改数据 5.多平 ...

  7. spark2的编译

    0.操作系统 centos:6.4 hadoop:2.5.0-cdh5.3.6 1.为什么要编译 spark 源码? 学习spark的第一步 就应该是编译源码,后期修改和调试,扩展集成的功能模块 2. ...

  8. 第六届蓝桥杯软件类省赛题解C++/Java

    第六届蓝桥杯软件类省赛题解C++/Java 1[C++].统计不含4的数字统计10000至99999中,不包含4的数值个数.答:暴力循环范围内所有数字判断一下就是了,答案是52488 1[Java]. ...

  9. 今天用node的cheerio模块做了个某乎的爬虫

        一时兴起,想做个爬虫,经过各种深思熟虑,最后选择了某乎,毕竟现在某乎的数据质量还是挺高的.说干就干 打开某乎首页,随便搜索了一串关键字,相关的问题和答案就展现在眼前,我就思考怎么把这些搜索结果 ...

  10. 微信公众号批量爬取java版

    最近需要爬取微信公众号的文章信息.在网上找了找发现微信公众号爬取的难点在于公众号文章链接在pc端是打不开的,要用微信的自带浏览器(拿到微信客户端补充的参数,才可以在其它平台打开),这就给爬虫程序造成很 ...