ExecutionContext(执行上下文)综述
- 1. 简介
- 2. 同步异步对比
- 3. 上下文的捕获和恢复
- 4. Flowing ExecutionContext vs Using SynchronizationContext
- 5. 如何适用于 async/await
- 6. 两者的关系
- 7. 说明
1. 简介
注意: 本篇文章讲述的是在 .Net Framework 环境下的分析, 但是我相信这与 .Net Core 设计思想是一致,但在实现上一定优化了很多。
下面开始本次讲述:
ExecutionContext 实际上只是线程相关其他上下文的容器。
- 有些上下文起辅助作用
- 有些上下文对 .Net 执行模型至关重要
ExecutionContext 与周围环境的信息有关,这意味着,代码正在运行时,它存储了与 当前环境 或 “context” 有关的数据。
周围环境: 代码执行处,可以访问到的变量、方法、属性等等。
2. 同步异步对比
在同步世界:
- 在许多系统中,此类“周围”的信息在线程本地存储(TLS)中维护,例如在
[ThreadStatic]
字段或ThreadLocal<T>
中。- 在同步世界中,这样的 thread-local 信息就足够了。
- 任何事情发生在该线程上,也就是不管在该线程上所处的堆栈结构是什么,正在执行什么方法,等等。
- 所有在该线程上运行的代码都可以查看和影响该线程特有的数据。
- 在同步世界中,这样的 thread-local 信息就足够了。
在异步世界,TLS变得无关紧要,同步异步对比:
- 同步
- 例如:
- 如果我先执行操作 A
- 然后执行操作 B
- 然后执行操作 C
- 则所有这三个操作都在同一线程上发生
- 因此所有这三个操作都受该线程上存储的周围环境数据的影响。
- 例如:
- 异步
- 例如:
- 我可能在一个线程上启动 A
- 然后在另一个线程上完成它
- 这样操作 B 可以在不同于 A 的线程上启动或运行
- 并且类似地使 C 可以在不同于 B 的线程上启动或运行。
- 这意味着我们用来控制执行细节的周围环境context不再可行,因为TLS不会“流”过这些异步点。
- Thread-local 存储特定于线程,这些异步操作并不与特定线程相关联。
- 但是,通常存在逻辑控制流,我们希望这些周围环境的数据与该控制流一起流动,以使周围环境的数据从一个线程移动到另一个线程
- 这就 需要 ExecutionContext 来完成这些操作。
- 例如:
3. 上下文的捕获和恢复
ExecutionContext 实际上是一个 state 包
- 用于从一个线程上捕获所有 state
- 然后在控制逻辑流的同时将其还原到另一个线程
ExecutionContext 是使用静态方法 Capture 捕获的:
// 周围环境的 state 捕获到 ec 中
ExecutionContext ec = ExecutionContext.Capture();
通过静态方法 Run ,在委托(Run方法的参数)调用时恢复 ExecutionContext
ExecutionContext.Run(ec, delegate
{
… // 这里的代码将上述 ec 的状态视为周围环境
}, null);
所有派生异步工作的方法都以这种方式捕获和还原 ExecutionContext 的。
- 带有“Unsafe”字样的方法除外,它们是不安全的,因为它们不传播 ExecutionContext
例如:
- 当您使用
Task.Run
时,对Run
的调用将从调用线程中捕获 ExecutionContext ,并将该 ExecutionContext 实例存储到Task
对象中 - 当提供给
Task.Run
的委托作为该Task
执行的一部分被调用时,它是使用存储的 ExecutionContext 通过ExecutionContext.Run
来完成的
以下所有异步API的执行都是捕获 ExecutionContext 并将其存储,然后在调用某些代码时再使用存储的 ExecutionContext。
Task.Run
ThreadPool.QueueUserWorkItem
Delegate.BeginInvoke
Stream.BeginRead
DispatcherSynchronizationContext.Post
- 任何其他异步API
当我们谈论“flowing ExecutionContext”时,我们实际上是在讨论:
- 在一个线程上获取周围环境状态
- 在稍后的某个时刻将该状态恢复到另一个线程上(需要执行提供的委托的线程)。
4. Flowing ExecutionContext vs Using SynchronizationContext
前面我们介绍了 SynchronizationContext
是如何调度线程的,现在,我们要进行进行一次对比:
- flowing ExecutionContext 在语义上与 capturing and posting to a SynchronizationContext 完全不同。
- 当 ExecutionContext 流动时,您是从一个线程捕获 state ,然后还原该 state
- 使提供的委托执行时处于周围环境 state
- 当您捕获并使用 SynchronizationContext 时,不会发生这种情况。
- 捕获部分是相同的,因为您要从当前线程中获取数据,但是随后用不同方式使用 state
SynchronizationContext.Post
只是使用捕获的状态来调用委托,而不是在调用委托时设置该状态为当前状态- 该委托在何时何地以及如何运行完全取决于Post方法的实现
5. 如何适用于 async/await
async
和 await
关键字背后的框架支持会自动与 ExecutionContext 和 SynchronizationContext 交互。
每当代码等待一个可等待项(awaitable),该可等待项(awaitable) 的 等待者(awaiter) 说尚未完成时
- 即等待者(awaiter) 的
IsCompleted
返回false
则该方法需要暂停,并通过等待者(awaiter) 的 continuation
来恢复。
等待者(awaiter) : 可以理解为
await
产生的 Task对象。
5.1. 实现方式
5.1.1. ExecutionContext
- 前面已经提到过了, ExecutionContext 需要从发出
await
的代码一直流到continuation
委托的执行。- 这是由框架自动处理的
- 当
async
方法即将挂起时,基础设施将捕获 ExecutionContext - 得到的委托交给等待者(awaiter) ,而且此等待者(awaiter) 具有对此 ExecutionContext 实例的引用,并将在恢复该方法时使用它。
- 由
ExecutionContext
带领,启用重要的周围环境信息,去流过 awaits 。
5.1.2. SynchronizationContext
该框架还支持 SynchronizationContext 。前述对 ExecutionContext 的支持内置于表示 async
方法的“构建器”中
- 例如
System.Runtime.CompilerServices.AsyncTaskMethodBuilder
- 即
await
/async
会被编译成执行码
并且这些构建器可确保 ExecutionContext 跨 await
点流动,无论使用哪种可等待项(awaitable)。
相反,对 SynchronizationContext 的支持内置在 awaiting
的且已经构建好的Task
和 Task<TResult>
中
自定义的等待者(awaiter) (比如 new Task(...)
)可以自己添加类似的逻辑,但是不会自动获得实例化时的SynchronizationContext
- 这是设计使然,因为能够自定义何时以及如何调用 continuation 是自定义Task有用的一部分原因。
5.2. 执行过程
5.2.1. SynchronizationContext 使用和控制
- 当您
await
一个 task 时,默认情况下,等待者(awaiter) 将捕获当前的 SynchronizationContext(如果有的话) - 在 task 完成时将
Post
这个前面提供的 continuation 委托并回到该 context 进行执行- 运行委托的:不是在完成了 task 的线程上,也不是在
ThreadPool
的线程上
- 运行委托的:不是在完成了 task 的线程上,也不是在
如果开发人员不希望这种封送处理行为,则可以通过更改在那里使用的 可等待项(awaitable) / 等待者(awaiter) 来控制它。
- 大多数情况,等待
Task
或Task<TResult>
就时采用上述方式 - 可以通过
await
方法task.ConfigureAwait(…)
的返回值来修改这种封送处理行为ConfigureAwait()
返回一个 可等待项(awaitable),它可以抑制此默认的封送处理行为。ConfigureAwait()
的唯一bool
类型参数continueOnCapturedContext
- 为 true ,那么将获得默认行为;
- 为 false ,则等待者(awaiter) 不检查 SynchronizationContext ,就像没有一样
- 注意: 当等待的任务完成时,无论
ConfigureAwait
如何,在恢复执行的线程上,运行时都会检查当前的 context ,以确定:- continuation 是否可以在此处同步运行
- continuation 是否必须从此处开始异步调度(scheduled asynchronously)
5.2.2. ExecutionContext 的流动无法控制
尽管 ConfigureAwait
提供了,用于改变 SynchronizationContext 行为的、显示的、与 await
相关的编程模型,但是没有用于抑制 ExecutionContext
流动的、与 await
相关的编程模型支持。
- 这是故意的;
- 开发人员在编写异步代码时不必担心 ExecutionContext ;
- 它在基础架构级别上的支持,有助于在异步环境中模拟同步方式的语义(即TLS);
6. 两者的关系
7. 说明
SynchronizationContext 不是 ExecutionContext 的一部分吗?
- ExecutionContext 能够带着所有的上下文(例如 SecurityContext , HostExecutionContext , CallContext 等)流动
- 确实也包括 SynchronizationContext
- 我个人认为,这是API设计的一个错误,自从它在许多版本的.NET中提出以来,就引起了一些问题
- 注意这个问题在 .Net Core 已经解决
- .Net Core 中的 ExecutionContext 已不包含任何其他 context
当您调用公共 ExecutionContext.Capture()
方法时,它将检查当前的 SynchronizationContext ,如果有,则将其存储到返回的 ExecutionContext 实例中。然后,当使用公共 ExecutionContext.Run(...)
方法时,在提供的委托执行期间,该捕获的 SynchronizationContext 被恢复为 Current 。
为什么这有问题?作为 ExecutionContext 的一部分而流动的 SynchronizationContext 更改了 SynchronizationContext.Current
的含义。
应该可以通过 SynchronizationContext.Current
返回到你最近调用 Current
时的环境
- 因此,如果
SynchronizationContext
流出,成为另一个线程的当前SynchronizationContext
,则SynchronizationContext.Current
就没有意义了,所以不是这样设计的。
7.1. 示例
解释此问题的一个示例,代码如下:
private async void button1_Click(object sender, EventArgs e)
{
button1.Text = await Task.Run(async delegate
{
string data = await DownloadAsync();
return Compute(data);
});
}
7.1.1. 运行过程解析
- 用户单击 button1 ,导致UI框架在UI线程上调用 button1_Click 事件;
- 然后,代码启动一个 WorkItem 在 ThreadPool 上运行(通过
Task.Run
);- WorkItem 在 ThreadPool介绍-异步调用方法 中提到;
- 这个 WorkItem 开始一些下载工作,并异步等待其完成;
- 在下载完成之后,ThreadPool 上的 WorkItem 进行一些密集型操作(
Compute(data)
); - 返回结果
- WorkItem 执行完成后,导致正在 UI线程 上等待的
Task
完成 - (下载得到结果,返回结果),成为 UI线程 等待完成的 ;
- 然后,UI线程 处理 button1_Click 方法的剩余部分: 保存计算结果到
button1.Text
属性。
7.1.2. 带来的思考
如果 SynchronizationContext 不作为 ExecutionContext 的一部分流动,我的预期就是有根据的。
如果 SynchronizationContext 流动了,无论如何,我将感到非常失望。
假设:SynchronizationContext 作为 ExecutionContext 的一部分流动:
Task.Run
在调用时捕获 ExecutionContext ,并使用它运行传递给它委托。这就意味着
Task.Run
调用时的当前 SynchronizationContext 将流动到Task
中,而且将在DownloadAsync
执行和等待结果期间成为当前 SynchronizationContext ,- 这意味着这个
await
将看到当前SynchronizationContext
,并Post
异步方法的其余部分作为一个 continuation 返回到 UI线程 上运行。
- 这意味着这个
这意味着我的
Compute
方法将在 UI线程 上运行,而不是在 ThreadPool 上运行,从而导致我的应用程序出现响应性问题。从实际结果来看这是不对的,假设执行的代码更像下面的
private async void button1_Click(object sender, EventArgs e)
{
string data = await DownloadAsync();
button1.Text = Compute(data);
}
实际: 现在,我们看看实际是如何处理的:
Task.Run(...)
这种异步Api的实现:
解读捕获(Capture)和运行(Run);
- ExecutionContext 实际上有两个
Capture
方法:- 但是只有一个是
public
,供外部使用 - 那个
internal
的方法,是 mscorlib 大多数公开的异步功能(如:Task.Run(...)
)所使用的一个- 这个方法有选择地允许调用方抑制捕获 SynchronizationContext 作为 ExecutionContext 的一部分;
- 但是只有一个是
- 与此相对应的是,
Run
方法的internal
重载也支持忽略存储在 ExecutionContext 中的 SynchronizationContext- 实际上是假装没有被捕获(此外,这也是 mscorlib 中大多数方法使用的重载)。
- ExecutionContext 实际上有两个
这意味着:
- 在 mscorlib 中几乎包含所有异步操作的核心实现,这里不会将 SynchronizationContext 作为 ExecutionContext 的一部分流动
- 位于其他地方的,任何异步操作的核心实现,都将使 SynchronizationContext 作为 ExecutionContext 的一部分流动。
标识 async
关键字方法的实现:
- 之前我曾提到,异步方法的 “builders” 是负责在
async
方法中流动 ExecutionContext 所使用的方式- 这些 builders 确实存在于 mscorlib 中,并且确实使用
internal
的重载做一些事情。
- 这些 builders 确实存在于 mscorlib 中,并且确实使用
- 同样的, SynchronizationContext 不会作为 ExecutionContext 的一部分流动穿过 awaits
- 此外,这与 task awaiters 如何支持 捕获 SynchronizationContext 和将其
Post
回来是分开的 - 实现方式: 为了帮助处理 ExecutionContext 带着 SynchronizationContext 流动的情况,
async
方法的基础设施尝试忽略由于流动而将 SynchronizationContexts 设置为 Current 。
- 此外,这与 task awaiters 如何支持 捕获 SynchronizationContext 和将其
- 简而言之,
SynchronizationContext.Current
不会“流动”穿过await
点。
参考资料:
《ExecutionContext vs SynchronizationContext》 --- Stephen Toub
ExecutionContext(执行上下文)综述的更多相关文章
- 执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext
原文连接:执行上下文与同步上下文 - .NET 并行编程 (microsoft.com) 执行上下文与同步上下文 斯蒂芬 6月15日, 2012 最近,我被问了几次关于 ExecutionContex ...
- 再看javascript执行上下文、变量对象
突然看到一篇远在2010年的老文,作者以章节的形式向我们介绍了ECMA-262-3的部分内容,主要涉及到执行上下文.变量对象.作用域.this等语言细节.内容短小而精悍,文风直白而严谨,读完有酣畅淋漓 ...
- Javascript 执行上下文 context&scope
执行上下文(Execution context) 执行上下文可以认为是 代码的执行环境. 1 当代码被载入的时候,js解释器 创建一个 全局的执行上下文. 2 当执行函数时,会创建一个 函数的执行上下 ...
- 一篇文章看懂JS执行上下文
壹 ❀ 引 我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如: function f1() { console.lo ...
- JavaScript的执行上下文,真没你想的那么难
作者:小土豆 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/2436173500265335 前言 在正文开始前,先来看 ...
- 一文弄懂js的执行上下文与执行上下文栈
目录 执行上下文与执行上下文栈 变量提升与函数提升 变量提升 函数提升 变量提升与函数提升的优先级 变量提升的一道题目引出var关键字与let关键字各自的特性 执行上下文 全局执行上下文 函数(局部) ...
- 从执行上下文(ES3,ES5)的角度来理解"闭包"
目录 介绍执行上下文和执行上下文栈概念 执行上下文 执行上下文栈 伪代码模拟分析以下代码中执行上下文栈的行为 代码模拟实现栈的执行过程 通过ES3提出的老概念-理解执行上下文 1.变量对象和活动对象 ...
- javascript 执行上下文的理解
首先,为什么某些函数以及变量在没有被声明以前就可以被使用,javascript引擎内部在执行代码以前到底做了些什么?这里,想信大家都会想到,变量声明提前这个概念: 但是,以下我要讲的是,声明提前的这个 ...
- Javascript本质第二篇:执行上下文
在上一篇文章<Javascript本质第一篇:核心概念>中,对Javascript执行上下文做了解释,但是这些都是基于Javascript标准中对执行上下文的定义,也就是说理论上的东西,本 ...
随机推荐
- 异数OS TCP协议栈测试(三)--长连接篇
异数OS TCP协议栈测试(三)--长连接篇 本文来自异数OS社区 github: 异数OS-织梦师(消息中间件)群: 476260389 异数OS TCP长连接技术简介 说起长连接,则首先要谈对 ...
- 51Nod 2026 Gcd and Lcm
题目传送门 分析: 开始玩一个小小的trick 我们发现\(f(n)=\sum_{d|n}\mu(d)\cdot d\)是一个积性函数 所以: \(~~~~f(n)=\prod f(p_i^{a_i} ...
- 20191211 HNOI2017模拟赛 C题
题目: 分析: 开始觉得是神仙题... 然后发现n最多有2个质因子 这说明sm呢... 学过物理的小朋友们知道,当一个物体受多个不同方向相同的力时,只有相邻力的夹角相等,受力就会平衡 于是拆扇叶相当于 ...
- SpringBoot实现简单的CRUD
CRUD-员工列表 实验要求: 1).RestfulCRUD:CRUD满足Rest风格: URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作 2).实验的请求架构; 3).员工列表 ...
- 关于Python类的多继承中的__mro__属性使用的C3算法以及继承顺序解释
刚刚学到类的多继承这个环节,当子类继承多个父类时,调用的父类中的方法具体是哪一个我们无从得知,为此,在Python中有函数__mro__来表示方法解析顺序. 当前Python3.x的类多重继承算法用的 ...
- 使用postman测试接口
1.什么是接口测试 其实接口测试就和普通功能测试没什么区别,区别就是功能测试是在页面上点点点,在页面上输入值,提交数据看结果,而接口测试没有页面,通过接口规范文档上的调用地址.请求参数,拼接报文,然后 ...
- java实现字符串翻转
public class StringReverse { /*一共写了三个函数func1 func2 func3 * 时间: 2019年9月12日9:00 * func1用的反向输出到一个新的字符串中 ...
- Web 开发工具类(2): HttpClientUtils
HttpClientUtils 整合了一些 web开发中常用的httpClient操作: package com.evan.common.utils; import java.io.IOExcepti ...
- CUDA学习(三)之使用GPU进行两个数组相加
传入两个数组,在GPU中将两个数组对应索引位置相加 #include "cuda_runtime.h" #include "device_launch_parameter ...
- OpenCV3入门(三)基本绘图函数
1.函数原型 /** @brief Draws a line segment connecting two points.*/ CV_EXPORTS_W void line(InputOutputAr ...