为什么LogicalCallContext不适用于异步?

问题描述:

在此 MSDN线程.

LogicalCallContext保持一个哈希表,该哈希表存储发送到CallContext.LogicalGet/SetData的数据.并且它仅对此Hashtable进行浅表复制.因此,如果将可变对象存储在其中,则不同的任务/线程将看到彼此的更改.这就是为什么Stephen Cleary的示例NDC程序(发布在该MSDN线程上)无法正常工作的原因.

LogicalCallContext keeps a Hashtable storing the data sent to CallContext.LogicalGet/SetData. And it only does a shallow copy of this Hashtable. So if you store a mutable object in it, different tasks/threads will see each other's changes. This is why Stephen Cleary's example NDC program (posted on that MSDN thread) doesn't work correctly.

但是AFAICS,如果您仅将不可变数据存储在哈希表中(也许使用

But AFAICS, if you only store immutable data in the Hashtable (perhaps by using immutable collections), that should work, and let us implement an NDC.

但是,斯蒂芬·克雷里(Stephen Cleary)在接受的答案中也说:

However, Stephen Cleary also said in that accepted answer:

CallContext不能用于此目的.微软专门推荐禁止将CallContext用于除远程处理以外的任何操作.更重要的是,逻辑CallContext无法理解异步方法如何早日返回并随后恢复.

CallContext can't be used for this. Microsoft has specifically recommended against using CallContext for anything except remoting. More to the point, the logical CallContext doesn't understand how async methods return early and resume later.

不幸的是,指向Microsoft建议的链接已关闭(找不到页面).所以我的问题是,为什么不建议这样做?为什么不能以这种方式使用LogicalCallContext?说它不理解异步方法是什么意思?从调用者的POV来看,它们只是返回Tasks的方法,不是吗?

Unfortunately, that link to the Microsoft recommendation is down (page not found). So my question is, why is this not recommended? Why can't I use LogicalCallContext in this way? What does it mean to say it doesn't understand async methods? From the caller's POV they are just methods returning Tasks, no?

ETA:另请参见其他问题.在那里,斯蒂芬·克莱里(Stephen Cleary)的回答是:

ETA: see also this other question. There, an answer by Stephen Cleary says:

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

you could use CallContext.LogicalSetData and CallContext.LogicalGetData, but I recommend you don't because they don't support any kind of "cloning" when you use simple parallelism

这似乎支持了我的观点.因此,我应该能够构建一个NDC,这实际上是我所需要的,而不仅仅是log4net.

That seems to support my case. So I should be able to build an NDC, which is in fact what I need, just not for log4net.

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

I wrote some sample code and it seems to work, but mere testing doesn't always catch concurrency bugs. So since there are hints in those other posts that this may not work, I'm still asking: is this approach valid?

ETA:当我从下面的答案中运行Stephen的拟议副本时,我没有得到他说的错误答案,我得到了正确答案.即使在他说这里的LogicalCallContext值始终为" 1"的地方,我也始终会得到正确的值0.这是否可能是由于竞争条件造成的?无论如何,我仍然没有在自己的计算机上重现任何实际问题.这是我正在运行的确切代码;它在此处仅打印"true",Stephen表示至少应在某些时候打印"false".

ETA: When I run Stephen's proposed repro from the answer below), I don't get the wrong answers he says I would, I get correct answers. Even where he said "LogicalCallContext value here is always "1"", I always get the correct value of 0. Is this perhaps due to a race condition? Anyway, I've still not reproduced any actual problem on my own computer. Here's the exact code I'm running; it prints only "true" here, where Stephen says it should print "false" at least some of the time.

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);
  }

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

So my restated question is, what (if anything) is wrong with the above code?

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

Furthermore, when I look at the code for CallContext.LogicalSetData, it calls Thread.CurrentThread.GetMutableExecutionContext() and modifies that. And GetMutableExecutionContext says:

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

然后CreateMutableCopy最终对LogicalCallContext的Hashtable进行浅表复制,该Hashtable包含用户提供的数据.

And CreateMutableCopy eventually does a shallow copy of the LogicalCallContext's Hashtable that holds the user-supplied data.

因此,试图理解为什么这段代码不适用于Stephen,是因为ExecutionContextBelongsToCurrentScope有时具有错误的值吗?如果真是这样,也许我们可以注意到它的所作所为-通过查看当前任务ID或当前线程ID发生了变化-并手动将单独的值存储在我们不变的结构中,以线程+任务ID为键. (这种方法存在性能问题,例如,为死任务保留数据,但除此之外还能行得通吗?)

So trying to understand why this code doesn't work for Stephen, is it because ExecutionContextBelongsToCurrentScope has the wrong value sometimes? If that's the case, maybe we can notice when it does - by seeing that either the current task ID or the current thread ID have changed - and manually store separate values in our immutable structure, keyed by thread + task ID. (There are performance issues with this approach, e.g. the retention of data for dead tasks, but apart from that would it work?)

Stephen确认此方法适用于.Net 4.5和Win8/2012.尚未在其他平台上进行测试,并且至少在某些平台上无法运行.因此,答案是微软将他们的游戏放在一起,并至少在最新版本的.Net和异步编译器中解决了根本问题.

Stephen confirms that this works on .Net 4.5 and Win8/2012. Not tested on other platforms, and known not to work on at least some of them. So the answer is that Microsoft got their game together and fixed the underlying issue in at least the most recent version of .Net and the async compiler.

因此答案是,它确实有效,只是在较旧的.Net版本上不起作用. (因此,log4net项目不能使用它来提供通用的NDC.)

So the answer is, it does work, just not on older .Net versions. (So the log4net project can't use it to provide a generic NDC.)