AsyncLocal 的语义与逻辑调用上下文有何不同?

How do the semantics of AsyncLocal differ from the logical call context?

.NET 4.6 引入了 AsyncLocal<T> class 用于沿着异步控制流流动环境数据。我以前为此目的使用过 CallContext.LogicalGet/SetData,我想知道这两者在语义上是否以及以何种方式不同(除了明显的 API 差异,如强类型和不依赖字符串键) .

I'm wondering if and in what ways the two are semantically different

从中可以看出,CallContextAsyncLocal 内部都依赖 ExecutionContext 将其内部数据存储在 Dictionary 中。后者似乎为异步调用添加了另一个间接级别。 CallContext 自 .NET Remoting 以来一直存在,并且是在没有真正替代方法的异步调用之间传输数据的便捷方式,直到现在。

我能发现的最大区别是 AsyncLocal 现在允许您在基础存储值发生更改时通过回调注册通知,可以通过 ExecutionContext 开关或显式替换现有值价值。

// AsyncLocal<T> also provides optional notifications 
// when the value associated with the current thread
// changes, either because it was explicitly changed 
// by setting the Value property, or implicitly changed
// when the thread encountered an "await" or other context transition.
// For example, we might want our
// current culture to be communicated to the OS as well:

static AsyncLocal<Culture> s_currentCulture = new AsyncLocal<Culture>(
args =>
{
    NativeMethods.SetThreadCulture(args.CurrentValue.LCID);
});

除此之外,一个位于 System.Threading 而另一个位于 System.Runtime.Remoting ,CoreCLR 将支持前者。

此外,AsyncLocal 似乎没有 SetLogicalData 具有的浅层写时复制语义,因此数据在调用之间流动而不会被复制。

语义几乎相同。两者都存储在 ExecutionContext 中并通过异步调用流动。

不同之处在于 API 更改(正如您所描述的)以及为值更改注册回调的能力。

从技术上讲,实现上有很大的不同,因为每次复制 CallContext 时都会克隆它(使用 CallContext.Clone),而 AsyncLocal 的数据保存在ExecutionContext._localValues 字典,仅复制该参考文献,无需任何额外工作。

为确保更新仅在您更改 AsyncLocal 的值时影响当前流,将创建一个新字典并将所有现有值浅复制到新字典。

根据使用 AsyncLocal 的位置,这种差异对性能可能有利有弊。

现在,正如 Hans Passant 在评论中提到的那样 CallContext 最初是为远程处理而制作的,在不支持远程处理的地方(例如 .Net Core)不可用,这可能就是为什么 AsyncLocal 添加到框架中:

#if FEATURE_REMOTING
    public LogicalCallContext.Reader LogicalCallContext 
    {
        [SecurityCritical]
        get { return new LogicalCallContext.Reader(IsNull ? null : m_ec.LogicalCallContext); } 
    }

    public IllogicalCallContext.Reader IllogicalCallContext 
    {
        [SecurityCritical]
        get { return new IllogicalCallContext.Reader(IsNull ? null : m_ec.IllogicalCallContext); } 
    }
#endif

注意:Visual Studio SDK 中还有一个 AsyncLocal,它基本上是 CallContext 的包装器,显示了概念的相似程度:Microsoft.VisualStudio.Threading.

时间上似乎有一些语义差异。

使用 CallContext 时,上下文更改会在子 thread/task/async 方法的上下文设置时发生,即当调用 Task.Factory.StartNew()、Task.Run() 或异步方法时。

使用 AsyncLocal,当子 thread/task/async 方法实际开始执行时,上下文更改(调用更改通知回调)发生。

时间差异可能很有趣,特别是如果您希望在切换上下文时克隆上下文对象。使用不同的机制可能会导致克隆不同的内容:使用 CallContext,您可以在创建子 thread/task 或调用异步方法时克隆内容;但是使用 AsyncLocal,当子 thread/task/async 方法开始执行时克隆内容,上下文对象的内容可能已被父线程更改。