ICriticalNotifyCompletion是用来做什么的?

15
我试图理解C#异步机制的工作原理,其中一个令人困惑的来源是ICriticalNotifyCompletion接口。
该接口提供两种方法:从INotifyCompletion继承的OnCompleted(Action)和UnsafeOnCompleted(Action)。文档对两种方法都有完全相同的描述:“安排在实例完成时调用的继续操作。”。
唯一的区别是关于UnsafeOnCompleted(Action)的备注指出,它不必“传播ExecutionContext信息”,这是什么意思。没有明确说明OnCompleted(Action)必须“传播ExecutionContext信息”。 可等待表达式的文档说明,如果awaiter实现了ICriticalNotifyCompletion,则会调用UnsafeOnCompleted(Action)。那么实现ICriticalNotifyCompletion是否使OnCompleted(Action)变得多余?
为什么awaiter要实现ICriticalNotifyCompletion?TaskAwaiter实现ICriticalNotifyCompletion的影响是什么?如果没有实现会怎样?

Async Task types提案中找到了更多细节。但实际原因可能是由微软工程师解释的,我想。 - Pavel Anikhouski
1个回答

8
“传播ExecutionContext信息”,无论这是什么意思。
实际上没有关于这个的好的详尽定义,我肯定不会尝试提供一个,因为我知道我会漏掉一些重要的内容。不过,我确实知道,流动ExecutionContext对于安全原因是必要的——这就是为什么所有不流动上下文的方法都使用Unsafe命名约定。Stephen Toub在他的博客中提到: “ExecutionContext涉及“环境”信息,也就是说,它存储与运行当前环境或“上下文”相关的数据……ExecutionContext包含的上下文之一是SecurityContext,它保留诸如当前“principal”以及代码访问安全性(CAS)拒绝和许可等信息。”
所以首先要认识到的是,ExecutionContext必须被传递。另外,还有一个问题,Stephen Toub在他的博客中描述了这个问题。为了历史背景,这个描述是在async/await技术还是预发布阶段的时候(但是可以公开使用):
“许多人没有意识到他们的awaiter需要传播ExecutionContext,以确保上下文在await点之间流动……因此,我们修改了Framework中的async方法构建器(例如AsyncTaskMethodBuilder)……这些构建器现在自己在await点之间传递ExecutionContext,从awaiter身上解除了这个责任。”
最初,awaiter会传播ExecutionContext,但在官方async/await发布之前进行了更改,所以构建器会传播ExecutionContext。当然,这意味着awaiter不再需要传播ExecutionContext;如果他们这样做了,异步代码将会在两个地方传播它(其中“flow”是一个“capture”,后跟一个“在这个捕获的上下文中执行委托”)。
现在有足够的信息来回答以下问题:
“为什么awaiter要实现ICriticalNotifyCompletion?TaskAwaiter实现ICriticalNotifyCompletion的影响是什么?如果没有实现会怎样?”
如果awaiter没有实现ICriticalNotifyCompletion,则使用该代码的await将导致ExecutionContext被传递两次(一次由awaiter,一次由async方法构建器)。这并不会破坏任何东西,只是效率不如可能的高。
“这样实现ICriticalNotifyCompletion是否使OnCompleted(Action)变得多余?”
不完全正确。再次参考Stephen Toub的文章

如果您正在构建一个带有AllowPartiallyTrustedCallersAttribute (APTCA)属性的程序集,则需要确保从您的程序集公开的任何API在异步点之间正确地流经ExecutionContext...否则可能存在安全漏洞。由于awaiter类型通常是在APTCA程序集中实现的,而OnCompleted可能会被用户直接调用(尽管它实际上是要被编译器使用的),因此OnCompleted需要流经ExecutionContext...我们还有UnsafeOnCompleted,它不需要流经ExecutionContext,但也标记为SecurityCritical,使得部分信任的代码无法调用它。

结论是:

如果您正在实现自己的awaiter,请尽可能同时实现INotifyCompletion和ICriticalNotifyCompletion,在前者中流经ExecutionContext,在后者中不流经。唯一不实现两者的好理由是:当您在无法流经ExecutionContext的情况下实现awaiter时,例如部分受信任的awaiter或您没有使用ExecutionContext的能力,或者您的awaiter依赖的API没有任何选项可以控制是否流经上下文...在这种情况下,只需实现INotifyCompletion即可。

我只稍微修改了这个结论。自上述文章发布以来,已经过去了将近十年,我认为很少使用APTCA程序集。即.NET Core根本不支持部分信任。对于.NET Core,我认为您可以说OnCompleted是多余的。然而,在.NET Framework世界中,这种区别仍然很重要,因为OnCompleted对于部分受信任程序集中的awaiter是必需的。

所以我的建议是:在实现awaiter时,如果可以不流动ExecutionContext,请始终实现ICriticalNotifyCompletion。否则,只需保留常规的OnCompleted实现。


我有一种感觉,虽然在技术上编写自己的任务类型是可能的,但这并不是你应该做的事情。 - TrayMan
@TrayMan:同意。在高性能场景下,它们偶尔是有用的,但现在.NET中有了ValueTask<T>/ManualResetValueTaskSourceCore<T>,我认为它们未来的用途不大。 - Stephen Cleary
1
在阅读了几乎所有的Stephen Toub的帖子后,我偶然发现了这个。我越来越感觉到,部分信任根本就不能可靠地工作;那些东西似乎是所有雷区的母亲。 - dialer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接