原文:https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/

作者:Stephen

翻译:xiaoxiaotank

不来深入了解一下?

为了更好的理解本文内容,强烈建议先看一下理解C#中的ConfigureAwait

虽然原文发布于2012年,但是内容放到今日仍不过时。好,开始吧!

最近,有人问了我几个关于ExecutionContextSynchronizationContext的问题,例如:它们俩有什么区别?“流动”它们有什么意义?它们与C#和VB中新的async/await语法糖有什么关系?我想通过本文来解决其中一些问题。

注意:本文深入到了.NET的高级领域,大多数开发人员都无需关注。

什么是ExecutionContext,流动它有什么意义?

对于绝大多数开发者来说,不需要关注ExecutionContext。它的存在就像空气一样:虽然它很重要,但我们一般是不会关注它的,除非有必要(例如出现问题时)。ExecutionContext本质上只是一个用于盛放其他上下文的容器。这些被盛放的上下文中有一些仅仅是辅助性的,而另一些则对于.NET的执行模型至关重要,不过它们都和ExecutionContext一样:除非你不得不知道他们存在,或你正在做某些特别高级的事情,或者出了什么问题(,否则你没必要关注它)。

ExecutionContext是与“环境”信息相关的,也就是说它会存储与你当前正在运行的环境或“上下文”相关的数据。在许多系统中,这类环境信息使用线程本地存储(TLS)来维护,例如ThreadStatic标记的字段或ThreadLocal<T>。在同步的世界里,这种线程本地信息就足够了:所有的一切都运行在该线程上,因此,无论你在该线程上使用什么栈帧、正在执行什么功能,等等,在该线程上运行的所有代码都可以查看并受该线程特定数据的影响。例如,ExecutionContext盛放的一个上下文叫做SecurityContext,它维护了诸如当前“principal”之类的信息以及有关代码访问安全性(CAS)拒绝和允许的信息。这类信息可以与当前线程相关联,这样的话,如果一个栈帧的访问被某个权限拒绝了然后调用另一个方法,那么该调用的方法仍会被拒绝:当尝试执行需要该权限的操作时,CLR会检查当前线程是否允许该操作,并且它也会找到调用者放入的数据。

当从同步世界过渡到异步世界时,事情就变得复杂了起来。突然之间,TLS变得无关紧要。在同步的世界里,如果我先执行操作A,然后再执行操作B,最后执行操作C,那么这三个操作都会在同一线程上执行,所以这三个操作都会受该线程上存储的环境数据的影响。但是在异步的世界里,我可能在一个线程上启动A,然后在另一个线程上完成它,这样操作B就可以在不同于A的线程上启动或运行,并且类似地C也可以在不同于B的线程上启动或运行。 这意味着我们用来控制执行细节的环境不再可行,因为TLS不会在这些异步点上“流动”。线程本地存储特定于某个线程,而这些异步操作并不与特定线程绑定。不过,我们希望有一个逻辑控制流,且环境数据可以与该控制流一起流动,以便环境数据可以从一个线程移动到另一个线程。这就是ExecutionContext发挥的作用。

ExecutionContext实际上只是一个状态包,可用于从一个线程捕获所有当前状态,然后在控制逻辑继续流动的同时将其还原到另一个线程。通过静态Capture方法来捕获ExecutionContext

// 把环境状态捕捉到ec中
ExecutionContext ec = ExecutionContext.Capture();

在调用委托时,通过静态Run方法将环境状态还原回来:

ExecutionContext.Run(ec, delegate
{
… // 此处的代码会将ec的状态视为环境
}, null);

.NET Framework中所有异步工作的方法都是以这种方式捕获和还原ExecutionContext的(除了那些以“Unsafe”为前缀的方法,这些方法都是不安全的,因为它们显式的不流动ExecutionContext)。例如,当你使用Task.Run时,对Run的调用会导致捕获调用线程的ExecutionContext,并将该ExecutionContext实例存储到Task对象中。稍后,当传递给Task.Run的委托作为该Task执行的一部分被调用时,会通过调用ExecutionContext.Run方法,使委托在刚才存储的上下文中执行。Task.RunThreadPool.QueueUserWorkItemDelegate.BeginInvokeStream.BeginReadDispatcherSynchronizationContext.Post,以及你可以想到的任何其他异步API,都是这样的。它们全都会捕获ExecutionContext,存储起来,然后在调用某些代码时使用它。

当我们讨论“流动ExecutionContext”时,指的就是这个过程,即获取一个线程上的环境状态,然后在执行传递的委托时,将状态还原到执行线程上。

什么是SynchronizationContext,捕获和使用它有什么意义?

在软件开发中,我们喜欢抽象。我们几乎不会愿意对特定的实现进行硬编码,相反,在编写大型系统时,我们更原意将特定实现的细节抽象化,以便以后可以插入其他实现,而不必更改我们的大型系统。这就是我们有接口、抽象类,虚方法等的原因。

SynchronizationContext只是一种抽象,代表你要执行某些操作的特定环境。举个例子,WinForm拥有UI线程(虽然可能有多个,但出于讨论目的,这并不重要),需要使用UI控件的任何操作都需要在上面执行。为了处理需要先在线程池线程上执行然后再封送回UI线程,以便该操作可以与UI控件一起处理的情形,WinForm提供了Control.BeginInvoke方法。你可以向控件的BeginInvoke方法传递一个委托,该委托将在与该控件关联的线程上被调用。

因此,如果我正在编写一个需要在线程池线程执行一部分工作,然后在UI线程上再进行一部分工作的组件,那我可以使用Control.BeginInvoke。但是,如果我现在要在WPF应用程序中使用我的组件该怎么办?WPF具有与WinForm相同的UI线程约束,但封送回UI线程的机制不同:不是通过Control.BeginInvoke,而是在Dispatcher实例上调用Dispatcher.BeginInvoke(或InvokeAsync)。

现在,我们有两个不同的API用于实现相同的基本操作,那么如何编写与UI框架无关的组件呢?当然是通过使用SynchronizationContextSynchronizationContext提供了一个虚Post方法,该方法只接收一个委托,并在任何地点,任何时间运行它,当然SynchronizationContext的实现要认为是合适的。WinForm提供了WindowsFormSynchronizationContext类,该类重写了Post方法来调用Control.BeginInvoke。WPF提供了DispatcherSynchronizationContext类,该类重写Post方法来调用Dispatcher.BeginInvoke,等等。这样,我现在可以在组件中使用SynchronizationContext,而不需要将其绑定到特定框架。

如果我要专门针对WinForm编写组件,则可以像这样来实现先进入线程池,然后返回到UI线程的逻辑:

public static void DoWork(Control c)
{
ThreadPool.QueueUserWorkItem(delegate
{
… // 在线程池中执行 c.BeginInvoke(delegate
{
… // 在UI线程中执行
});
});
}

如果我把组件改成使用SynchronizationContext,就可以这样写:

public static void DoWork(SynchronizationContext sc)
{
ThreadPool.QueueUserWorkItem(delegate
{
… // 在线程池中执行 sc.Post(delegate
{
… // 在UI线程中执行
}, null);
});
}

当然,需要传递目标上下文(即sc)来返回显得很烦人(对于某些所需的编程模型而言,这是无法容忍的),因此,SynchronizationContext提供了Current属性,该属性使你可以从当前线程中寻找上下文,如果存在的话,它会把你返回到该环境。你可以这样“捕获”它(即从SynchronizationContext.Current中读取引用,并存储该引用以供以后使用):

public static void DoWork()
{
var sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate
{
… // 在线程池中执行 sc.Post(delegate
{
… // 在原始上下文中执行
}, null);
});
}

流动ExecutionContext vs 使用SynchronizationContext

现在,我们有一个非常重要的发现:流动ExecutionContext在语义上与捕获SynchronizationContext并Post完全不同。

当流动ExecutionContext时,你是从一个线程中捕获状态,然后在提供的委托执行期间将该状态恢复回来。而你捕获并使用SynchronizationContext时,不会出现这种情况。捕获部分是相同的,因为你要从当前线程中获取数据,但是后续使用状态的方式不同。SynchronizationContext是通过SynchronizationContext.Post来使用捕获的状态调用委托,而不是在委托调用期间将状态恢复为当前状态。该委托在何时何地以及如何运行完全取决于Post方法的实现。

这是如何运用于async/await的?

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

每当代码等待一个awaiter,awaiter说它尚未完成(例如awaiter.IsCompleted返回false)时,该方法需要暂停,然后通过awaiter的延续(Continuation)来恢复,这是我之前提到的异步点之一。因此,ExecutionContext需要从发出等待的代码一直流动到延续委托的执行,这会由框架自动处理。当异步方法即将挂起时,基础架构会捕获ExecutionContext。传递给awaiter的委托会拥有该ExecutionContext实例的引用,并在恢复该方法时使用它。这就是使ExecutionContext表示的重要“环境”信息跨等待流动的原因。

该框架还支持SynchronizationContext。前面对ExecutionContext的支持内置于表示异步方法的“构建器”中(例如System.Runtime.CompilerServices.AsyncTaskMethodBuilder),并且这些构建器可确保ExecutionContext跨等待点流动,而不管使用哪种等待方式。相反,对SynchronizationContext的支持已内置在等待TaskTask <TResult>的支持中。自定义awaiter可以自己添加类似的逻辑,但不会自动获取。这是设计使然,因为自定义何时以及后续如何调用是自定义awaiter使用的原因之一。

默认情况下,当你等待Task时,awaiter将捕获当前的SynchronizationContext,当Task完成时,会将提供的延续(Continuation)委托封送到该上下文去执行,而不是在任务完成的线程上,或在ThreadPool上执行该委托。如果开发人员不希望这种封送行为,则可以通过更改使用的awaiter来进行控制。虽然在等待TaskTask <TResult>时始终会采用这种行为,但你可以改为等待task.ConfigureAwait(…)ConfigureAwait方法返回一个awaitable,它可以阻止默认的封送处理行为。是否阻止由传递给ConfigureAwait方法的布尔值控制。如果continueOnCapturedContext为true,就是默认行为;否则,如果为false,那么awaiter不会检查SynchronizationContext,假装好像没有一样。(注意,待完成的Task完成后,无论ConfigureAwait如何,运行时(runtime)可能会检查正在恢复的线程上的当前上下文,以确定是否可以在此处同步运行延续,或必须从那时开始异步安排延续。)

注意,尽管ConfigureAwait为更改与SynchronizationContext相关的行为提供了显式的与等待相关的编程模型支持,但没有用于阻止ExecutionContext流动的与等待相关的编程模型支持,就是故意这样设计的。开发人员在编写异步代码时无需关注ExecutionContext。它在基础架构级别的支持,可帮助你在异步环境中模拟同步语义(即TLS)。大多数人可以并且应该完全忽略它的存在(除非他们真的知道自己在做什么,否则应避免使用ExecutionContext.SuppressFlow方法)。相反,开发人员应该意识到代码在哪里运行,因此SynchronizationContext上升到了值得显式编程模型支持的水平。(实际上,正如我在其他文章中所述,大多数类库开发者都应考虑在每次Task等待时使用ConfigureAwait(false)。)

SynchronizationContext不是ExecutionContext的一部分吗?

到目前为止,我掩盖了一些细节,但是我还是没法避免它们。

我掩盖的主要内容是ExecutionContext能够流动的所有上下文(例如SecurityContextHostExecutionContextCallContext等),SynchronizationContext实际上就是其中之一。我个人认为,这是API设计中的一个错误,这是自许多版本的.NET首次提出以来引起的一些问题。不过,这是我们已经使用了很长时间的设计,如果现在进行更改那将是一项中断性更改。

当你调用公共的ExecutionContext.Capture()方法时,该方法将检查当前的SynchronizationContext,如果有,则将其存储到返回的ExecutionContext实例中。然后,当你使用公共的ExecutionContext.Run方法时,在执行提供的委托期间,捕获的SynchronizationContext会被恢复为Current

这有什么问题?作为ExecutionContext的一部分流动的SynchronizationContext更改了SynchronizationContext.Current的含义。SynchronizationContext.Current应该可以使你返回到访问Current时所处的环境,因此,如果SynchronizationContext流到了另一个线程上成为Current,那么你就无法信任SynchronizationContext.Current的含义。在这种情况下,它可能用于返回到当前环境,也可能用于回到流中先前某个时刻所处的环境。(译注:一定要看到文章末尾,否则你可能会产生误解)

举一个可能出现这种问题的例子,请参考以下代码:

private void button1_Click(object sender, EventArgs e)
{
button1.Text = await Task.Run(async delegate
{
string data = await DownloadAsync();
return Compute(data);
});
}

我的思维模式告诉我,这段代码会发生这种情况:用户单击button1,导致UI框架在UI线程上调用button1_Click。然后,代码启动一个在ThreadPool上运行的操作(通过Task.Run)。该操作将开始一些下载工作,并异步等待其完成。然后,ThreadPool上的延续操作会对下载的结果进行一些计算密集型操作,并返回结果,最终使正在UI线程上等待的Task完成。接着,UI线程会处理该button1_Click方法的其余部分,并将计算结果存储到button1的Text属性中。

如果SynchronizationContext不会作为ExecutionContext的一部分流动,那么这是我所期望的。但是,如果流动了,我会感到非常失望。Task.Run会在调用时捕获ExecutionContext,并使用它来执行传递给它的委托。这意味着调用Task.Run时所处的UI线程的SynchronizationContext将流入Task,并且在await DownloadAsync时再次作为Current流入。这意味着await将会找到UI的SynchronizationContext.Current,并Post该方法的剩余部分作为在UI线程上运行的延续。也就表示我的Compute方法很可能会在UI线程上运行,而不是在ThreadPool上运行,从而导致我的应用程序出现响应问题。

现在,这个故事有点混乱了:ExecutionContext实际上有两个Capture方法,但是只公开了一个。mscorlib公开的大多数异步功能所使用的是内部的(mscorlib内部的)Capture方法,并且它可选地允许调用方阻止捕获SynchronizationContext作为ExecutionContext的一部分;对应于Run方法的内部重载也支持忽略存储在ExecutionContext中的SynchronizationContext,实际上是假装没有被捕获(同样,这是mscorlib中大多数功能使用的重载)。这意味着几乎所有在mscorlib中的异步操作的核心实现都不会将SynchronizationContext作为ExecutionContext的一部分进行流动,但是在其他任何地方的任何异步操作的核心实现都将捕获SynchronizationContext作为ExecutionContext的一部分。我上面提到了,异步方法的“构建器”是负责在异步方法中流动ExecutionContext的类型,这些构建器是存在于mscorlib中的,并且使用的确实是内部重载……(当然,这与task awaiter捕获SynchronizationContext并将其Post回去是互不影响的)。为了处理ExecutionContext确实流动了SynchronizationContext的情况,异步方法基础结构会尝试忽略由于流动而设置为CurrentSynchronizationContexts

简而言之,SynchronizationContext.Current不会在等待点之间“流动”,你放心好了。

理解C#中的ExecutionContext vs SynchronizationContext的更多相关文章

  1. 理解C#中的 async await

    前言 一个老掉牙的话题,园子里的相关优秀文章已经有很多了,我写这篇文章完全是想以自己的思维方式来谈一谈自己的理解.(PS:文中涉及到了大量反编译源码,需要静下心来细细品味) 从简单开始 为了更容易理解 ...

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

    原文连接:执行上下文与同步上下文 - .NET 并行编程 (microsoft.com) 执行上下文与同步上下文 斯蒂芬 6月15日, 2012 最近,我被问了几次关于 ExecutionContex ...

  3. 如何理解javaSript中函数的参数是按值传递

    本文是我基于红宝书<Javascript高级程序设计>中的第四章,4.1.3传递参数小节P70,进一步理解javaSript中函数的参数,当传递的参数是对象时的传递方式. (结合资料的个人 ...

  4. 怎么理解js中的事件委托

    怎么理解js中的事件委托 时间 2015-01-15 00:59:59  SegmentFault 原文  http://segmentfault.com/blog/sunchengli/119000 ...

  5. 如何理解T-SQL中Merge语句(二)

    写在前面的话:上一篇写了如何理解T-SQL中Merge语句,基本把Merge语句要讲的给讲了,在文章的后面,抛出了几个结,当时没有想明白怎么去用文字表达,这一篇就来解答一下这几个结,又是一篇“天马行空 ...

  6. 如何理解T-SQL中Merge语句

    写在前面的话:之前看过Merge语句,感觉没什么用,完全可以用其他的方式来替代,最近又看了看Merge语句,确实挺好用,可以少写很多代码,看起来也很紧凑,当然也有别的优点. ====正文开始===== ...

  7. 深入理解JDK中的I/O

    深入理解JDK中的I/O 目 录 java内存模型GCHTTP协议事务隔离级并发多线程设计模式清楚redis.memcache并且知道区别mysql分表分库有接口幂等性了解jdk8稍微了解一下特性 j ...

  8. 深度理解Jquery 中 offset() 方法

    参考原文:深度理解Jquery 中 offset() 方法

  9. 简单理解Struts2中拦截器与过滤器的区别及执行顺序

    简单理解Struts2中拦截器与过滤器的区别及执行顺序 当接收到一个httprequest , a) 当外部的httpservletrequest到来时 b) 初始到了servlet容器 传递给一个标 ...

随机推荐

  1. Magento add product attribute and assign to all group

    $attributes = array( 'product_type' => array( 'type' => 'int', 'input' => 'select', 'source ...

  2. 牛客网PAT练兵场-锤子剪刀布

    题目地址:https://www.nowcoder.com/questionTerminal/79db907555c24b15a9c73f7f7d0e2471 题解:无 /** * *作者:Ycute ...

  3. Java算法——分治法

         一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简 ...

  4. 从 BIO、NIO 聊到 Netty,最后还要实现个 RPC 框架!

    大家好,我是 「后端技术进阶」 作者,一个热爱技术的少年. 觉得不错的话,欢迎 star!ღ( ´・ᴗ・` )比心 Netty 从入门到实战系列文章地址:https://github.com/Snai ...

  5. 【原创】Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: KVM版本:5.9 ...

  6. 手写迷你Tomcat

    手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat手写迷你Tomcat ...

  7. ABP开发框架的技术点分析(1)

    ABP是ASP.NET Boilerplate的简称,ABP是一个开源且文档友好的应用程序框架.ABP不仅仅是一个框架,它还提供了一个最徍实践的基于领域驱动设计(DDD)的体系结构模型.ABP框架可以 ...

  8. oracle的system登不了

    (密码对的,密码错直接就是被拒了) 这个一直弹出改密码 但是改了点[确定],又说 oracle改system密码 [oracle@localhost ~]$ sqlplus / as sysdba S ...

  9. Labview学习之路(一)程序框图中的修饰

    很多小伙伴知道在前面板有很多修饰符,比如上凸框,加粗下凹框等等,但是其实在程序框图中也是有修饰符的,他的位置比较隐蔽,并且修饰符很少,所以很多人基本没有用过.现在就给大家介绍一些这些程序框图种的修饰. ...

  10. 【Android】AndroidStudio关于EventBus报错解决方法its super classes have no public methods with the @Subscribe

    作者:程序员小冰,GitHub主页:https://github.com/QQ986945193 新浪微博:http://weibo.com/mcxiaobing 首先说明,以前我用eventBus的 ...