Python线程5分钟完全解读
298
2022-08-16
ExecutionContext(执行上下文)综述(context of execution)
1. 简介
注意: 本篇文章讲述的是在 .Net Framework 环境下的分析, 但是我相信这与 .Net Core 设计思想是一致,但在实现上一定优化了很多。
下面开始本次讲述:
ExecutionContext 实际上只是线程相关其他上下文的容器。
有些上下文起辅助作用
有些上下文对 .Net 执行模型至关重要
ExecutionContext 与周围环境的信息有关,这意味着,代码正在运行时,它存储了与 当前环境 或 “context” 有关的数据。
周围环境: 代码执行处,可以访问到的变量、方法、属性等等。
2. 同步异步对比
在同步世界:
在许多系统中,此类“周围”的信息在线程本地存储(TLS)中维护,例如在 [ThreadStatic] 字段或 ThreadLocal
在同步世界中,这样的 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
自定义的等待者(awaiter) (比如 new Task(...))可以自己添加类似的逻辑,但是不会自动获得实例化时的SynchronizationContext
这是设计使然,因为能够自定义何时以及如何调用 continuation 是自定义Task有用的一部分原因。
5.2. 执行过程
5.2.1. SynchronizationContext 使用和控制
当您 await 一个 task 时,默认情况下,等待者(awaiter) 将捕获当前的 SynchronizationContext(如果有的话)
在 task 完成时将 Post 这个前面提供的 continuation 委托并回到该 context 进行执行
运行委托的:不是在完成了 task 的线程上,也不是在 ThreadPool 的线程上
如果开发人员不希望这种封送处理行为,则可以通过更改在那里使用的 可等待项(awaitable) / 等待者(awaiter) 来控制它。
大多数情况,等待 Task 或 Task
可以通过 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 中大多数方法使用的重载)。
这意味着:
在 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://bigbrotherstone.cnblogs.com/
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~