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

dan*_*mak 23 .net c# synchronization task-parallel-library async-await

在这个问题中,Stephen Cleary接受的答案是LogicalCallContext无法正常使用异步.他还在这个 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.

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

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);
  }
Run Code Online (Sandbox Code Playgroud)

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

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

if (!this.ExecutionContextBelongsToCurrentScope)
    this.m_ExecutionContext = this.m_ExecutionContext.CreateMutableCopy();
  this.ExecutionContextBelongsToCurrentScope = true;
Run Code Online (Sandbox Code Playgroud)

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

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

Ste*_*ary 17

更新:这个答案对于.NET 4.5不正确.有关详细信息,请参阅我的博文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").
}
Run Code Online (Sandbox Code Playgroud)

但是,一旦你使用我所谓的"简单并行"(开始几种async方法,然后使用Task.WaitAll或类似),事情就不那么好了.这是一个类似于我的MSDN论坛帖子的示例(为简单起见,假设一个非并行的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").
}
Run Code Online (Sandbox Code Playgroud)

的问题是,LogicalCallContext共享之间ParentAsync,ChildAAsync以及ChildBAsync,没有任何方式挂接到或强制深复制操作.在"线性"示例中,上下文也是共享的,但一次只有一个方法处于活动状态.

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

我已经详细研究了这一点,并得出结论,解决方案是不可能的.如果你能想出一个,我会很高兴被证明是错的.:)

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

  • 在这里查看您的答案/编辑/评论,很难说出对所有这些的“最新”判决是什么。:) 简而言之,我的发现是 LogicalCallContext 适用于线性异步调用(在 .NET 4.5 中),但不适用于简单的并行性(即 Task.WhenAll)。简而言之,这差不多吗? (2认同)

dan*_*mak 9

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

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