在这个问题中,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 为键。(这种方法存在性能问题,例如保留死任务的数据,但除此之外它还能工作吗?)