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

.NET 4.6 introduces the AsyncLocal<T> class for flowing ambient data along the asynchronous flow of control. I've previously used CallContext.LogicalGet/SetData for this purpose, and I'm wondering if and in what ways the two are semantically different (beyond the obvious API differences like strong typing and lack of reliance on string keys).

19666 次浏览

我想知道这两者在语义上是否有区别,以及在哪些方面有区别

可以看出,CallContextAsyncLocal在内部都依赖于 ExecutionContextDictionary中存储它们的内部数据。后者似乎为异步调用增加了另一个间接级别。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

注意: VisualStudioSDK 中还有一个 AsyncLocal,它基本上是 CallContext上的一个包装器,它显示了这些概念有多么相似: Microsoft VisualStudio 线程

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

使用 CallContext,当子线程/任务/异步方法的上下文被设置时,上下文就会发生变化,例如,当 Task。工厂。StartNew () ,Task.调用 Run ()或异步方法。

使用 AsyncLocal 时,上下文更改(调用更改通知回调)发生在子线程/任务/异步方法实际开始执行时。

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