执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext

发布时间:2022-06-28 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

原文连接:执行上下文与同步上下文 - .NET 并行编程 (microsoft.com)

执行上下文与同步上下文

执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext

斯蒂芬

6月15日, 2012

最近,我被问了几次关于 ExecutionContext 和 SynchronizationContext 的各种问题,例如它们之间的区别是什么,"流动"它们意味着什么,以及它们与 C# 和 Visual Basic 中新的 async/await 关键字的关系。我想我会尝试在这里解决其中的一些问题。

警告:本文深入探讨了大多数开发人员永远不需要考虑的 .NET 高级领域。

 

什么是 ExecutionContext,流动它意味着什么?

 

ExecutionContext是绝大多数开发人员永远不需要考虑的事情之一。它有点像空气:它在那里很重要,但除了在一些关键时刻(例如,当它出现问题时),我们不会考虑它在那里。ExecutionContext 实际上只是其他上下文的容器。其中一些其他上下文是辅助的,而有些上下文对 .NET 的执行模型至关重要,但它们都遵循我为 ExecutionContext 描述的相同理念:如果你必须知道它们在那里,要么你正在做一些超级高级的事情,要么出了什么问题。

ExecutionContext 是关于"环境"信息的,这意味着它存储与当前环境或运行其中的"上下文"相关的数据。在许多系统中,此类环境信息保存在线程本地存储 (TLS) 中,例如在 ThreadStatic 字段或 ThreadLocal<T>中。在同步世界中,这样的线程本地信息就足够了:一切都发生在该线程上,因此无论您在该线程上的哪个堆栈帧,正在执行什么函数等等,在该线程上运行的所有代码都可以看到并受到特定于该线程的数据的影响。例如,ExecutionContext 包含的上下文之一是 SecurityContext,它维护当前"主体"等信息以及有关代码访问安全性 (CAS) 拒绝和允许的信息。此类信息可以与当前线程相关联,这样,如果一个堆栈帧拒绝访问某个权限,然后调用另一个方法,则该调用方法仍将受到线程上设置的拒绝的约束:当它尝试执行需要该权限的操作时,CLR 将检查当前线程的拒绝以查看是否允许该操作, 它会找到调用方放在那里的数据。

当您从同步世界移动到异步世界时,事情会变得更加复杂。突然之间,TLS在很大程度上变得无关紧要。在同步世界中,如果我执行操作 A,然后执行操作 B,然后执行操作 C,则所有这三个操作都发生在同一线程上,因此所有这三个操作都受该线程上存储的环境数据的影响。但是在异步世界中,我可能会在一个线程上启动 A,然后在另一个线程上完成它,这样操作 B 可能会在与 A 不同的线程上启动或运行,同样,C 可以在与 B 不同的线程上启动或运行。这意味着我们用来控制执行细节的这种环境上下文不再可行,因为TLS不会在这些异步点之间"流动"。线程本地存储特定于线程,而这些异步操作不绑定到特定线程。但是,通常存在一个逻辑控制流,我们希望此环境数据与该控制流一起流动,以便环境数据从一个线程移动到另一个线程。这就是 ExecutionContext 所启用的功能。

ExecutionContext 实际上只是一个状态包,可用于从一个线程捕获所有这些状态,然后在逻辑控制流继续时将其还原到另一个线程上。ExecutionContext 使用静态 Capture 方法捕获:

环境状态捕获到ec ExecutionContext ec = ExecutionContext.Capture();

并且在调用委托期间通过静态 run 方法还原它:

ExecutionContext.Run(ec,delegate{ ... // 这里的代码会将ec 的状态视为 ambient }, null);

.NET Framework 中分叉异步工作的所有方法都以类似的方式捕获和还原 ExecutionContext(也就是说,除了那些以"不安全"为前缀的方法之外的所有方法都是不安全的,因为它们显式不流出 ExecutionContext)。例如,使用 Task.Run 时,对 Run 的调用将从调用线程捕获 ExecutionContext,并将该 ExecutionContext 实例存储到 Task 对象中。当提供给 Task.Run 的委托稍后作为该任务执行的一部分被调用时,它将通过使用存储的上下文通过 ExecutionContext.Run 完成。对于 Task.Run、ThreadPool.QueueUserWorkItem、Delegate.BeginInvoke、Stream.BeginRead、DispatcherSynchronizationContext.Post 以及您能想到的任何其他异步 API 都是如此。它们都捕获 ExecutionContext,存储它,然后在以后调用某些代码期间使用存储的上下文。

当我们谈论"流动执行上下文"时,我们谈论的正是这样一个过程,即获取一个线程上的环境状态,并在稍后的某个时间点将该状态还原到线程上,同时该线程执行提供的委托。

这两张图片是我自己画的,和原作者没关系。

执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext

 

 

 

执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext

 

什么是同步上下文,捕获和使用它意味着什么?

我们从事软件开发工作,喜欢抽象。我们很少乐于对特定实现进行硬编码;相反,在编写更高级别的系统时,我们抽象出特定实现的细节,以便我们可以在以后插入不同的实现,而不必更改我们的更高级别的系统。这就是为什么我们有接口,这就是为什么我们有抽象类,这就是为什么我们有虚拟方法,等等。

SynchronizationContext 只是一个抽象,它表示要在其中执行某些工作的特定环境。作为此类环境的一个示例,Windows 窗体应用具有一个 UI 线程(虽然可能有多个线程,但出于本讨论的目的,这无关紧要),这是需要使用 UI 控件的任何工作需要发生的地方。对于在 ThreadPool 线程上运行代码并且需要将工作封送回 UI 以便此工作可以使用 UI 控件进行混杂的情况,Windows 窗体提供了 Control.BeginInvoke 方法。您为控件的 BeginInvoke 方法提供委托,该委托将在与该控件关联的线程上重新调用。

因此,如果我正在编写一个组件,该组件需要将一些工作安排到ThreadPool,然后继续在UI线程上进行一些工作,我可以将我的组件编码为使用Control.BeginInvoke。但是,现在,如果我决定要在 WPF 应用中使用我的组件,该怎么办?WPF 具有与 Windows 窗体相同的 UI 线程约束,但它具有不同的机制来封送回 UI 线程:您不是在与正确线程关联的控件上使用 Control.BeginInvoke,而是在与正确线程关联的调度程序实例上使用 Dispatcher.BeginInvoke(或 InvokeAsync)。

我们现在有两个不同的API来实现相同的基本操作,那么如何编写我的组件与UI框架无关呢?通过使用同步上下文。同步上下文提供了一个虚拟的 Post 方法;此方法只需获取委托,并在任何位置、任何时间运行它,并且 SyncContext 实现认为合适。Windows Forms 提供了 WindowsFormSynchronizationContext 类型,该类型覆盖 Post 以调用 Control.BeginInvoke。WPF 提供 DispatcherSynchronizationContext 类型,该类型覆盖 Post 以调用 Dispatcher.BeginInvoke。等等。因此,我现在可以对组件进行编码以使用SynceoContext,而不是将其绑定到特定的框架。

如果我专门编写面向 Windows 窗体的组件,我可能会实现我的转到线程池,然后返回到 UI 线程逻辑,如下所示:

public static
void DoWork(Control
c)
{ ThreadPool.QueueUserWorkItem(delegate {

... // do work

on ThreadPool c.BeginInvoke(delegate { ... // do work on UI
});
});
}

如果我改为将我的组件编写为使用同步上下文,我可能会将其编写为:

public
static void DoWork(SynchronizationContext
sc) { ThreadPool.QueueUserWorkItem(delegate
{

... // do work on ThreadPool sc.Post(delegate

{ ... //
do work on UI }, null);
});
}

当然,需要传递要返回的目标上下文是很烦人的(对于某些所需的编程模型来说,这是令人望而却步的),因此ConfictionContext提供了Current属性,该属性允许您从当前线程中发现允许您返回当前环境的上下文(如果有)。这允许您"捕获它"(即从SynceoxContext.Current读取引用并存储该引用以供以后使用):

public static void DoWork()
{
var sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate

{ ... // do
work on ThreadPool sc.Post(delegate

{ ... // 在原始
上下文上工作 }, null);
});
}

流式执行上下文与使用同步上下文

现在,我们有一个非常重要的观察结果:流式 ExecutionContext 在语义上与捕获并发布到同步上下文非常不同。

当您流经 ExecutionContext 时,您将从一个线程捕获状态,然后还原该状态,以便在提供的委托执行期间它是环境状态。当您捕获并使用同步上下文时,不会发生这种情况。捕获部分是相同的,因为您正在从当前线程中获取数据,但随后您将以不同的方式使用该状态。在调用委托期间,您不必使该状态成为当前状态,而是使用 SynchronizationContext.Post 您只需使用该捕获的状态来调用委托即可。该委托在何处、何时以及如何运行完全取决于 Post 方法的实现。

这如何应用于异步/等待?

异步和 await 关键字背后的框架支持会自动与 ExecutionContext 和 SynchronizationContext 交互。

每当代码等待一个等待者说它尚未完成(即等待者的IsCompleted返回false)时,该方法需要暂停,并且它将通过等待者的延续来恢复。这是我之前提到的异步点之一,因此,ExecutionContext 需要从发出 await 的代码流向延续委托的执行。这由框架自动处理。当异步方法即将挂起时,基础结构将捕获执行上下文。传递给等待程序的委托具有对此 ExecutionContext 实例的引用,并将在恢复该方法时使用它。这就是使 ExecutionContext 所代表的重要"环境"信息能够在 await 之间流动的原因。

该框架还支持同步上下文。前面提到的对 ExecutionContext 的支持内置于表示异步方法的"构建器"(例如 System.Runtime.CompilerServices.AsyncTaskMethodBuilder),这些构建器可确保 ExecutionContext 在等待点之间流动,而不管使用哪种等待点。相比之下,对SynceosingContext的支持内置于对等待任务和任务<TResult>的支持中。自定义等待者可以自己添加类似的逻辑,但他们不会自动获得它;这是设计使然,因为能够自定义何时以及如何调用延续是自定义等待器有用的部分原因。

当您等待任务时,默认情况下,等待者将捕获当前的SyncicousContext,如果存在,则当任务完成时,它会将提供的延续委托发布回该上下文,而不是在任务完成的任何线程上运行委托,或者计划它在ThreadPool上运行。如果开发人员不希望出现此封送处理行为,可以通过更改使用的等待/等待程序来控制它。虽然在等待 Task 或 Task<TResult>时始终使用此行为,但您可以改为等待调用任务的结果。ConfigureAwait(...).方法返回一个可等待对象,该值允许禁止显示此默认封送处理行为。它是否被抑制由传递给 ConfigureAwait 方法的布尔值控制。如果 continueOnCapturedContext 为 true,则得到默认行为;如果为 false,则等待者不会检查同步上下文,假装没有同步上下文。(请注意,当等待的任务完成时,无论 ConfigureAwait 如何,运行时都可以检查恢复线程上当前的上下文,以确定是否可以在那里同步运行延续,或者是否必须从该点异步调度延续。

请注意,虽然 ConfigureAwait 为更改与 SyncContext 相关的行为提供了显式的、与 await 相关的编程模型支持,但没有与 await 相关的编程模型支持来抑制 ExecutionContext 流。这是故意的。ExecutionContext不是编写异步代码的开发人员应该担心的事情;它的基础设施级支持有助于在异步世界中模拟同步语义(即TLS)。大多数人可以而且应该完全忽略它的存在(并且应该避免使用DevilcutionContext.SuppressFlow方法,除非他们真的知道你在做什么)。相比之下,代码运行是开发人员应该意识到的事情,因此SynceoContext上升到值得显式编程模型支持的水平。(事实上,正如我在其他文章中所说,大多数库实现者应该考虑在每次等待任务时使用配置Await(false)。

同步上下文不是 ExecutionContext 的一部分吗?

到目前为止,我已经掩盖了一些细节,但我无法进一步避免它们。

我掩饰的主要事情是,在所有执行上下文能够流动的上下文中(例如SecurityContext,HostExecutionContext,CallContext等),SyncingContext实际上是其中之一。我个人认为,这是API设计中的一个错误,自从它在许多版本之前在.NET中建立以来,它导致了一些问题。然而,这是我们长期以来的设计,现在改变它将是一个突破性的变化。

调用公共 ExecutionContext.Capture() 方法时,该方法将检查当前的同步上下文,如果存在,则将其存储到返回的 ExecutionContext 实例中。然后,当使用公共 ExecutionContext.Run 方法时,捕获的 SyncContext 将在执行提供的委托期间恢复为"当前"。

为什么这有问题?FlowIng SynchronizationContext 作为 ExecutionContext 的一部分,更改了 SynchronizationContext.Current 的含义。SynchronizationContext.Current 应该是您可以访问的内容,以返回到您访问 Current 时当前所处的环境,因此,如果 SynchronizationContext 在另一个线程上流动为当前环境,则无法信任 SynchronizationContext.Current 的含义。在这种情况下,它可能是返回当前环境的方式,也可能是返回到流中以前某个点发生的某个环境的方法。

private void button1_Click(object sender, EventArgs
e) {
button1.Text = await Task.Run(async delegate
{
string data = await DownloadAsync();
返回计算(数据);
});
}

这是我的心智模型告诉我的这个代码会发生什么。用户单击 button1,导致 UI 框架调用 UI 线程上的button1_Click。然后,该代码启动一个工作项,以便在 ThreadPool 上运行(通过 Task.Run)。该工作项启动一些下载工作,并异步等待其完成。然后,ThreadPool 上的后续工作项会对该下载的结果执行一些计算密集型操作,并返回结果,从而导致 UI 线程上正在等待的任务完成。此时,UI 线程处理此button1_Click方法的其余部分,将计算结果存储到 button1 的 Text 属性中。

如果同步上下文不作为 ExecutionContext 的一部分流动,我的期望是有效的。然而,如果它确实流动,我将非常失望。Task.Run 在调用时捕获 ExecutionContext,并使用它来运行传递给它的委托。这意味着,调用 Task.Run 时处于当前状态的 UI 同步上下文将流入任务,并且在调用 DownloadAsync 并等待生成的任务时将变为"当前"。这意味着 await 将看到"当前同步上下文",并将异步方法的其余部分作为在 UI 线程上运行的延续。这意味着我的 Compute 方法很可能会在 UI 线程上运行,而不是在 ThreadPool 上运行,从而导致我的应用出现响应能力问题。

故事现在变得有点混乱:ExecutionContext实际上有两种捕获方法,但其中只有一种是公开的。内部的(mscorlib的内部)是从mscorlib公开的大多数异步功能使用的那个,它可以选择允许调用者禁止捕获SynceoContext作为DeviltionContext的一部分;与此相对应的是,Run 方法的内部重载支持忽略存储在 ExecutionContext 中的 SynchronizationContext,实际上假装未捕获同步上下文(这再次是 mscorlib 中大多数功能使用的重载)。这意味着,几乎任何核心实现驻留在 mscorlib 中的异步操作都不会将 SynchronizationContext 作为 ExecutionContext 的一部分进行流动,但其核心实现驻留在其他任何地方的任何异步操作都将作为 ExecutionContext 的一部分流动 SynchronizationContext。我之前提到过,异步方法的"构建器"是负责在异步方法中流动 ExecutionContext 的类型,这些构建器确实存在于 mscorlib 中,并且它们确实使用内部重载...因此,SyncContext 不会作为 ExecutionContext 的一部分在 await 之间流动(这同样与任务等待者支持捕获 SynchronizationContext 并回传到它的方式是分开的)。为了帮助处理 ExecutionContext 确实流淌 SyncContext 的情况,异步方法基础结构会尝试忽略由于流式处理而设置为"当前"的同步上下文。

简而言之,SynchronizationContext.Current 不会在等待点之间"流动"。

作为此问题如何导致问题的一个示例,请考虑以下代码:

脚本宝典总结

以上是脚本宝典为你收集整理的执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext全部内容,希望文章能够帮你解决执行上下文与同步上下文 | ExecutionContext 和 SynchronizationContext所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签:并发