27

在这个问题中,Stephen Cleary 接受的答案说 LogicalCallContext 不能与异步一起正常工作。他还在MSDN 线程中发布了有关它的信息。

LogicalCallContext 保持一个 Hashtable 存储发送到 CallContext.LogicalGet/SetData 的数据。它只做这个 Hashtable 的浅拷贝。所以如果你在其中存储一个可变对象,不同的任务/线程会看到彼此的变化。这就是 Stephen Cleary 的示例 NDC 程序(发布在该 MSDN 线程上)无法正常工作的原因。

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

然而,斯蒂芬克利里也在接受的答案中说:

CallContext 不能用于此。Microsoft 特别建议不要将 CallContext 用于除远程处理之外的任何事情。更重要的是,逻辑 CallContext 不了解异步方法如何提前返回并稍后恢复。

不幸的是,指向 Microsoft 建议的链接已关闭(未找到页面)。所以我的问题是,为什么不推荐这样做?为什么我不能以这种方式使用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 进行浅拷贝。

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

4

2 回答 2

19

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

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

  • LogicalCallContextasync随叫随到;您可以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或类似的方法) ,事情就不那么好了。这是一个类似于我的 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").
}

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

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

我对此进行了详细调查,并得出结论认为不可能有解决方案。如果你能想出一个,我会很高兴被证明是错误的。:)

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

于 2013-01-06T01:01:19.483 回答
9

Stephen 确认这适用于 .Net 4.5 和 Win8/2012。未在其他平台上进行测试,并且已知至少不能在其中一些平台上工作。所以答案是微软将他们的游戏整合在一起,并在至少最新版本的 .Net 和异步编译器中修复了潜在问题。

所以答案是,它确实有效,只是不适用于较旧的 .Net 版本。(所以 log4net 项目不能用它来提供一个通用的 NDC。)

于 2013-01-07T09:32:11.340 回答