ExecutionContext(执行上下文)综述

>>返回《C# 并发编程》

  • 1. 简介
  • 2. 同步异步对比
  • 3. 上下文的捕获和恢复
  • 4. Flowing ExecutionContext vs Using SynchronizationContext
  • 5. 如何适用于 async/await
    • 5.1. 实现方式

      • 5.1.1. ExecutionContext
      • 5.1.2. SynchronizationContext
    • 5.2. 执行过程
      • 5.2.1. SynchronizationContext 使用和控制
      • 5.2.2. ExecutionContext 的流动无法控制
  • 6. 两者的关系
  • 7. 说明
    • 7.1. 示例

      • 7.1.1. 运行过程解析
      • 7.1.2. 带来的思考

1. 简介

注意: 本篇文章讲述的是在 .Net Framework 环境下的分析, 但是我相信这与 .Net Core 设计思想是一致,但在实现上一定优化了很多。

下面开始本次讲述:

ExecutionContext 实际上只是线程相关其他上下文的容器。

  • 有些上下文起辅助作用
  • 有些上下文对 .Net 执行模型至关重要

ExecutionContext周围环境的信息有关,这意味着,代码正在运行时,它存储了与 当前环境 或 “context” 有关的数据。

周围环境: 代码执行处,可以访问到的变量、方法、属性等等。

2. 同步异步对比

同步世界

  • 在许多系统中,此类“周围”的信息在线程本地存储(TLS)中维护,例如在 [ThreadStatic] 字段或 ThreadLocal<T> 中。

    • 同步世界中,这样的 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

asyncawait 关键字背后的框架支持会自动与 ExecutionContextSynchronizationContext 交互。

每当代码等待一个可等待项(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 会被编译成执行码

并且这些构建器可确保 ExecutionContextawait 点流动,无论使用哪种可等待项(awaitable)

相反,对 SynchronizationContext 的支持内置在 awaiting 的且已经构建好的TaskTask<TResult>

自定义的等待者(awaiter) (比如 new Task(...))可以自己添加类似的逻辑,但是不会自动获得实例化时的SynchronizationContext

  • 这是设计使然,因为能够自定义何时以及如何调用 continuation 是自定义Task有用的一部分原因。

5.2. 执行过程

5.2.1. SynchronizationContext 使用和控制

  • 当您 await 一个 task 时,默认情况下,等待者(awaiter) 将捕获当前的 SynchronizationContext(如果有的话)
  • task 完成时将 Post 这个前面提供的 continuation 委托并回到该 context 进行执行
    • 运行委托的:不是在完成了 task 的线程上,也不是在 ThreadPool 的线程上

如果开发人员不希望这种封送处理行为,则可以通过更改在那里使用的 可等待项(awaitable) / 等待者(awaiter) 来控制它。

  • 大多数情况,等待 TaskTask<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 能够带着所有的上下文(例如 SecurityContextHostExecutionContextCallContext 等)流动

    • 确实也包括 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 事件;
  • 然后,代码启动一个 WorkItemThreadPool 上运行(通过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 中大多数方法使用的重载)。
  • 这意味着:
    • mscorlib 中几乎包含所有异步操作的核心实现,这里不会将 SynchronizationContext 作为 ExecutionContext 的一部分流动
    • 位于其他地方的,任何异步操作的核心实现,都将使 SynchronizationContext 作为 ExecutionContext 的一部分流动。

标识 async 关键字方法的实现:

  • 之前我曾提到,异步方法的 “builders” 是负责在 async 方法中流动 ExecutionContext 所使用的方式

    • 这些 builders 确实存在于 mscorlib 中,并且确实使用 internal 的重载做一些事情。
  • 同样的, SynchronizationContext 不会作为 ExecutionContext 的一部分流动穿过 awaits
    • 此外,这与 task awaiters 如何支持 捕获 SynchronizationContext 和将其 Post 回来是分开的
    • 实现方式: 为了帮助处理 ExecutionContext 带着 SynchronizationContext 流动的情况, async 方法的基础设施尝试忽略由于流动而将 SynchronizationContexts 设置为 Current
  • 简而言之,SynchronizationContext.Current 不会“流动”穿过 await 点。

参考资料
《ExecutionContext vs SynchronizationContext》 --- Stephen Toub

原文地址:https://www.cnblogs.com/BigBrotherStone/p/12316599.html

时间: 2024-10-13 05:34:51

ExecutionContext(执行上下文)综述的相关文章

Javascript 执行上下文 context&amp;scope

执行上下文(Execution context) 执行上下文可以认为是 代码的执行环境. 1 当代码被载入的时候,js解释器 创建一个 全局的执行上下文. 2 当执行函数时,会创建一个 函数的执行上下文. 3 当执行 eval()的时候,创建 一个 eval 执行上下文. # if,for,while 等块不会创建 execution context,从而不会创建 scope. 当js解释器开始工作的时候: 1 首先创建一个 执行上下文栈(后进先出) 2 接着创建一个 全局的执行上下文,并放入执

再看javascript执行上下文、变量对象

突然看到一篇远在2010年的老文,作者以章节的形式向我们介绍了ECMA-262-3的部分内容,主要涉及到执行上下文.变量对象.作用域.this等语言细节.内容短小而精悍,文风直白而严谨,读完有酣畅淋漓.醍醐灌顶之感,强烈推荐!!! 原文链接:这里 本想翻译成文,原来早已有人做了,这里.真生不逢时,何其遗憾啊! 做个笔记,聊慰我心. 执行上下文 ExecutionContext 每当控制器(control)转换到ECMAScript可执行代码时,都会创建并进入到一个可执行上下文. 一段简短的句子,

一篇文章看懂JS执行上下文

 壹 ? 引 我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如: function f1() { console.log('听风是风'); }; f1(); //echo function f1() { console.log('echo'); }; f1(); //echo 按照代码书写顺序,应该先输出 听风是风,再输出 echo才对,很遗憾,两次输出均为 echo:如果我们将上述代码中的函数声明改为函数表达式,

javascript之执行上下文

执行上下文 一个执行上下文可以抽象成一个简单对象.每个执行上下文有系列的属性(我们可以叫做上下文的状态)来跟踪关联代码的处理. 下面的图是一个上下文的结构: 除了这三个必须的属性(变量对象,this对象和作用域链),一个执行上下文可能有其他的附加状态依赖于实现.

执行上下文对象的原理及使用

执行上下文对象: 在浏览器执行javascript代码之前,浏览器会做一些准备工作(从准备工作这一操作开始,直到对应的这一作用域的所有代码被执行完,这样的一个过程就叫做执行上下文;执行上下文可以被看成一个对象,这个对象 就是用来管理其对应作用域中的各个数据,这些数据就是对象中的属性). 一. 全局作用域中的一些准备工作     1. 找到标记的全局变量,并为其赋值为undefined;     2. 给this赋值为window对象     3. 函数声明,并给函数赋值为整个函数块 二. 函数作

JS高级 -- 执行上下文与作用域链

这个问题涉及到三个点: 1. 执行上下文 2. 函数嵌套导致的执行上下文栈 3.闭包 1 <script> 2 var a = 1; 3 var f1 = function(){//第一个函数 4 var a = 2; 5 var b = 1; 6 7 var f2 = function(){//第二个函数 8 var c = 1; 9 var f3 = function(){//第三个函数 //第三个函数执行,他自己的执行上下文中没有a,b,c,则从父级函数f2的执行上下文中去找,f2中有c

进阶学习js中的执行上下文

在js中的执行上下文,菜鸟入门基础 这篇文章中我们简单的讲解了js中的上下文,今天我们就更进一步的讲解js中的执行上下文. 1.当遇到变量名和函数名相同的问题. var a = 10; function a(){ console.log(1); } a(); //报错 如果你觉得函数a会覆盖变量a那你肯定是js的新朋友,为什么这里会报错呢?我记得我在基础的执行上下文文章中说过变量声明提前的概念,对这里就是因为这个原因,但是在上面一篇文章中还有一个问题我没有去讲. 那就是函数的声明比变量的声明顺序

执行上下文的过程

执行上下文的过程 每当调用一个函数时,一个新的执行上下文就会被创建出来.然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段: 1.建立阶段(发生在当调用一个函数时,但是在执行函数体内的具体代码以前) 建立变量,函数,arguments对象,参数 建立作用域链 确定this的值 具体过程如下: 1.找到当前上下文中的调用函数的代码 2.在执行被调用的函数体中的代码以前,开始创建执行上下文 3.进入第一个阶段-建立阶段: 建立variableObject对象: 建立argum

javascript之执行上下文堆栈

执行上下文堆栈 有三种类型的ECMAScript代码:全局代码,函数代码和eval代码.代码执行在它的执行上下文里. 有唯一的全局上下文,以及可能有多个函数和eval上下文.每一个函数调用,进入到函数的执行上 下文,执行函数的代码.当调用到 eval 函数, 进入到 eval 执行上下文,执行它的代码. 注意到,一个函数或许产生有限个上下文集合,因为每调用一个函数(甚至自身调用)产生一个 新的上下文,并伴随着新的上下文状态: function foo( bar ){} //调用同一函数 //生成