在此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键入。 (这种方法存在性能问题,例如保留死亡任务的数据,但除此之外是否有效?)
答案 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").
}
问题是LogicalCallContext
,ParentAsync
和ChildAAsync
之间ChildBAsync
共享,没有任何方式可以挂钩或强制深层复制操作。在“线性”示例中,上下文也是共享的,但一次只有一个方法处于活动状态。
即使您在LogicalCallContext
中存储的数据是不可变的(如在我的整数示例中),您仍然必须更新LogicalCallContext
值才能实现NDC,这意味着无共享的共享问题会让它变得混乱。
我已详细研究过这个问题,并得出结论认为解决方案是不可能的。如果你能想出一个,我会很高兴被证明是错的。 :)
P.S。 Stephen Toub指出,仅将CallContext
用于远程处理(无理由给出,IIRC)的建议不再适用。我们可以随意使用LogicalCallContext
...如果我们可以让它发挥作用。 ;)
答案 1 :(得分:9)
Stephen确认这适用于.Net 4.5和Win8 / 2012。未在其他平台上进行测试,并且已知其至少部分平台无法使用。所以答案是微软将他们的游戏放在一起,并至少在最新版本的.Net和异步编译器中修复了底层问题。
所以答案是,它确实有效,而不是旧的.Net版本。 (因此log4net项目不能使用它来提供通用的NDC。)