为什么LogicalCallContext无法与异步一起使用?

在这个问题中,Stephen Cleary接受的答案是LogicalCallContext无法与async一起正常工作。 他还在这个 MSDN主题中发布了它。

LogicalCallContext保持Hashtable存储发送到CallContext.LogicalGet / SetData的数据。 它只是这个Hashtable的浅层副本。 因此,如果您在其中存储可变对象,则不同的任务/线程将看到彼此的更改。 这就是Stephen Cleary的示例NDC程序(在MSDN线程上发布)无法正常工作的原因。

但是AFAICS,如果你只在Hashtable中存储不可变数据(可能通过使用不可变集合 ),那应该有效,让我们实现一个NDC。

然而,Stephen Cleary也在接受的答案中说:

CallContext不能用于此。 Microsoft特别建议不要使用CallContext进行远程处理以外的任何操作。 更重要的是,逻辑CallContext不了解异步方法如何提前返回并稍后恢复。

不幸的是,该建议的链接已关闭(找不到页面)。 所以我的问题是,为什么不推荐这个? 为什么我不能以这种方式使用LogicalCallContext? 说它不理解异步方法是什么意思? 从调用者的POV来看,他们只是返回任务的方法,不是吗?

ETA:另见其他问题 。 在那里,Stephen Cleary的答案说:

你可以使用CallContext.LogicalSetData和CallContext.LogicalGetData,但我建议你不要,因为当你使用简单的并行性时它们不支持任何类型的“克隆”

这似乎支持我的情况。 所以我应该能够建立一个NDC,这实际上是我需要的,而不是log4net。

我写了一些示例代码,它似乎工作,但仅仅测试并不总是捕获并发错误。 所以,由于其他post中有提示这可能不起作用,我仍然会问:这种方法有效吗?

ETA:当我从下面的答案中运行斯蒂芬提出的复制品时,我没有得到错误的答案,他说我会,我得到正确的答案。 即使他说“这里的LogicalCallContext值总是”1“”,我总是得到0的正确值。这可能是因为竞争条件? 无论如何,我还没有在我自己的电脑上复制任何实际问题。 这是我正在运行的确切代码; 它只在这里打印“真实”,斯蒂芬说它至少应该在某些时候打印“假”。

private static string key2 = "key2"; private static int Storage2 { get { return (int) CallContext.LogicalGetData(key2); } set { CallContext.LogicalSetData(key2, value);} } private static async Task ParentAsync() { //Storage = new Stored(0); // Set LogicalCallContext value to "0". Storage2 = 0; Task childTaskA = ChildAAsync(); // LogicalCallContext value here is always "1". // -- No, I get 0 Console.WriteLine(Storage2 == 0); Task childTaskB = ChildBAsync(); // LogicalCallContext value here is always "2". // -- No, I get 0 Console.WriteLine(Storage2 == 0); await Task.WhenAll(childTaskA, childTaskB); // LogicalCallContext value here may be "0" or "1". // -- I always get 0 Console.WriteLine(Storage2 == 0); } private static async Task ChildAAsync() { var value = Storage2; // Save LogicalCallContext value (always "0"). Storage2 = 1; // Set LogicalCallContext value to "1". await Task.Delay(1000); // LogicalCallContext value here may be "1" or "2". Console.WriteLine(Storage2 == 1); Storage2 = value; // Restore original LogicalCallContext value (always "0"). } private static async Task ChildBAsync() { var value = Storage2; // Save LogicalCallContext value (always "1"). Storage2 = 2; // Set LogicalCallContext value to "2". await Task.Delay(1000); // LogicalCallContext value here may be "0" or "2". Console.WriteLine(Storage2 == 2); Storage2 = value; // Restore original LogicalCallContext value (always "1"). } public static void Main(string[] args) { try { ParentAsync().Wait(); } catch (Exception e) { Console.WriteLine(e); } 

所以我重申的问题是,上述代码有什么问题(如果有的话)?

此外,当我查看CallContext.LogicalSetData的代码时,它调用Thread.CurrentThread.GetMutableExecutionContext()并修改它。 而GetMutableExecutionContext说:

 if (!this.ExecutionContextBelongsToCurrentScope) this.m_ExecutionContext = this.m_ExecutionContext.CreateMutableCopy(); this.ExecutionContextBelongsToCurrentScope = true; 

并且CreateMutableCopy最终会执行LogicalCallContext的Hashtable的浅表副本,该副本包含用户提供的数据。

所以试图理解为什么这段代码对Stephen不起作用,是因为ExecutionContextBelongsToCurrentScope有时会出错? 如果是这种情况,也许我们可以注意到它 – 通过查看当前任务ID或当前线程ID已更改 – 并在我们的不可变结构中手动存储单独的值,由线程+任务ID键入。 (这种方法存在性能问题,例如保留死亡任务的数据,但除此之外是否有效?)

更新:这个答案对于.NET 4.5不正确。 有关详细信息,请参阅我在AsyncLocal上的博客文章 。

这是情况(在你的问题中重复几点):

  • LogicalCallContext将与async调用一起流动; 您可以使用它来设置一些隐式数据,并从调用堆栈中的async方法中读取它。
  • LogicalCallContext所有副本都是浅拷贝,没有任何方式可以让最终用户代码挂钩到深拷贝类操作。
  • 当您使用async进行“简单并行”时,各种async方法之间只共享一个LogicalCallContext副本。

如果您的async代码全部是线性的, LogicalCallContext 可以正常工作:

 async Task ParentAsync() { ... = 0; // Set LogicalCallContext value to "0". await ChildAAsync(); // LogicalCallContext value here is always "0". await ChildBAsync(); // LogicalCallContext value here is always "0". } async Task ChildAAsync() { int value = ...; // Save LogicalCallContext value (always "0"). ... = 1; // Set LogicalCallContext value to "1". await Task.Delay(1000); // LogicalCallContext value here is always "1". ... = value; // Restore original LogicalCallContext value (always "0"). } async Task ChildBAsync() { int value = ...; // Save LogicalCallContext value (always "0"). ... = 2; // Set LogicalCallContext value to "2". await Task.Delay(1000); // LogicalCallContext value here is always "2". ... = value; // Restore original LogicalCallContext value (always "0"). } 

但是,一旦你使用我称之为“简单并行”的东西(启动几个async方法,然后使用Task.WaitAll或类似方法),事情就不那么好了。 这是一个类似于我的MSDN论坛post的示例(为简单起见,假设一个非并行的SynchronizationContext ,如GUI或ASP.NET):

编辑:代码注释不正确; 看到关于这个问题和答案的评论

 async Task ParentAsync() { ... = 0; // Set LogicalCallContext value to "0". Task childTaskA = ChildAAsync(); // LogicalCallContext value here is always "1". Task childTaskB = ChildBAsync(); // LogicalCallContext value here is always "2". await Task.WhenAll(childTaskA, childTaskB); // LogicalCallContext value here may be "0" or "1". } async Task ChildAAsync() { int value = ...; // Save LogicalCallContext value (always "0"). ... = 1; // Set LogicalCallContext value to "1". await Task.Delay(1000); // LogicalCallContext value here may be "1" or "2". ... = value; // Restore original LogicalCallContext value (always "0"). } async Task ChildBAsync() { int value = ...; // Save LogicalCallContext value (always "1"). ... = 2; // Set LogicalCallContext value to "2". await Task.Delay(1000); // LogicalCallContext value here may be "0" or "2". ... = value; // Restore original LogicalCallContext value (always "1"). } 

问题是LogicalCallContextParentAsyncChildAAsyncChildBAsync之间共享 ,没有任何方法可以挂钩或强制执行深层复制操作。 在“线性”示例中,上下文也是共享的,但一次只有一个方法处于活动状态。

即使您存储在LogicalCallContext的数据是不可变的(如在我的整数示例中),您仍然必须更新LogicalCallContext值以实现NDC,这意味着无共享的共享问题将使它变得混乱起来。

我已经详细研究过这个问题,并得出结论认为解决方案是不可能的。 如果你能想出一个,我会很高兴被certificate是错的。 🙂

PS Stephen Toub指出,仅使用CallContext进行远程处理的建议(无理由给出,IIRC)不再适用。 我们可以随意使用LogicalCallContext …如果我们可以让它工作。 ;)

斯蒂芬证实这适用于.Net 4.5和Win8 / 2012。 没有在其他平台上测试过,并且已知至少不能在其他平台上工作。 所以答案是微软将他们的游戏放在一起并至少在最新版本的.Net和异步编译器中修复了底层问题。

所以答案是,它确实有效,而不是旧的.Net版本。 (因此log4net项目不能使用它来提供通用的NDC。)