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...如果我们可以让它工作.;)
斯蒂芬证实这适用于.Net 4.5和Win8/2012.未在其他平台上进行测试,并且已知其至少部分平台无法使用.所以答案是微软将他们的游戏放在一起并至少在最新版本的.Net和异步编译器中修复了底层问题.
所以答案是,它确实有效,而不是旧的.Net版本.(因此log4net项目不能使用它来提供通用的NDC.)