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

时间:2013-01-05 20:22:31

标签: c# .net synchronization task-parallel-library async-await

在此question中,Stephen Cleary接受的答案表明LogicalCallContext无法正常使用异步。他还在this MSDN主题中发布了它。

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

但AFAICS,如果您只在Hashtable中存储不可变数据(可能使用immutable collections),那应该有效,并让我们实现NDC。

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

  

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

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

ETA:另见this other question。在那里,Stephen Cleary的答案说:

  

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

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

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

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键入。 (这种方法存在性能问题,例如保留死亡任务的数据,但除此之外是否有效?)

2 个答案:

答案 0 :(得分:17)

更新:对于.NET 4.5,此答案不正确。有关详细信息,请参阅my blog post on AsyncLocal

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

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

async 可以正常工作:

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或类似方法),事情就不那么好了。这是一个类似于my MSDN forum 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").
}

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

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

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

P.S。 Stephen Toub指出,仅将CallContext用于远程处理(无理由给出,IIRC)的建议不再适用。我们可以随意使用LogicalCallContext ...如果我们可以让它发挥作用。 ;)

答案 1 :(得分:9)

Stephen确认这适用于.Net 4.5和Win8 / 2012。未在其他平台上进行测试,并且已知其至少部分平台无法使用。所以答案是微软将他们的游戏放在一起,并至少在最新版本的.Net和异步编译器中修复了底层问题。

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