LogicalOperationStack与.Net 4.5中的异步不兼容

Trace.CorrelationManager.LogicalOperationStack允许嵌套逻辑操作标识符,其中最常见的情况是日志记录(NDC)。 是否仍然可以使用async-await

这是一个使用LogicalFlow的简单示例,它是我在LogicalOperationStack简单包装器:

 private static void Main() => OuterOperationAsync().GetAwaiter().GetResult(); private static async Task OuterOperationAsync() { Console.WriteLine(LogicalFlow.CurrentOperationId); using (LogicalFlow.StartScope()) { Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); await InnerOperationAsync(); Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); await InnerOperationAsync(); Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); } Console.WriteLine(LogicalFlow.CurrentOperationId); } private static async Task InnerOperationAsync() { using (LogicalFlow.StartScope()) { await Task.Delay(100); } } 

LogicalFlow

 public static class LogicalFlow { public static Guid CurrentOperationId => Trace.CorrelationManager.LogicalOperationStack.Count > 0 ? (Guid) Trace.CorrelationManager.LogicalOperationStack.Peek() : Guid.Empty; public static IDisposable StartScope() { Trace.CorrelationManager.StartLogicalOperation(); return new Stopper(); } private static void StopScope() => Trace.CorrelationManager.StopLogicalOperation(); private class Stopper : IDisposable { private bool _isDisposed; public void Dispose() { if (!_isDisposed) { StopScope(); _isDisposed = true; } } } } 

输出:

 00000000-0000-0000-0000-000000000000 49985135-1e39-404c-834a-9f12026d9b65 54674452-e1c5-4b1b-91ed-6bd6ea725b98 c6ec00fd-bff8-4bde-bf70-e073b6714ae5 54674452-e1c5-4b1b-91ed-6bd6ea725b98 

具体值并不重要,但据我所知,外线应显示Guid.Empty (即00000000-0000-0000-0000-000000000000 ),内线应显示相同的Guid值。

您可能会说LogicalOperationStack正在使用不是线程安全的Stack ,这就是输出错误的原因。 虽然这一般都是正确的,但在这种情况下,同时访问LogicalOperationStack线程永远不会超过一个 (在调用时等待每个async操作,并且不使用任何组合器,例如Task.WhenAll

问题是LogicalOperationStack存储在具有写时复制行为的CallContext中。 这意味着只要您没有在CallContext显式设置某些内容(当您使用StartLogicalOperation添加到现有堆栈时就没有),您将使用父上下文而不是您自己的上下文。

这可以通过在添加到现有堆栈之前将任何内容设置到CallContext来显示。 例如,如果我们将StartScope更改为:

 public static IDisposable StartScope() { CallContext.LogicalSetData("Bar", "Arnon"); Trace.CorrelationManager.StartLogicalOperation(); return new Stopper(); } 

输出是:

 00000000-0000-0000-0000-000000000000 fdc22318-53ef-4ae5-83ff-6c3e3864e37a fdc22318-53ef-4ae5-83ff-6c3e3864e37a fdc22318-53ef-4ae5-83ff-6c3e3864e37a 00000000-0000-0000-0000-000000000000 

注意: 我不是建议任何人真正这样做。 真正实用的解决方案是使用ImmutableStack而不是LogicalOperationStack因为它既是线程安全的,因为当你调用Pop时它是不可变的,你会得到一个新的ImmutableStack ,然后你需要将它设置回CallContext 作为这个问题的答案,可以使用完整的实现: 跟踪c#/ .NET任务流程

那么, LogicalOperationStack应该使用async并且它只是一个错误吗? LogicalOperationStack是不是意味着async世界? 或者我错过了什么?


更新 :使用Task.Delay显然令人困惑,因为它使用System.Threading.Timer 在内部捕获ExecutionContext 。 使用await Task.Yield(); 而不是await Task.Delay(100); 使示例更容易理解。

是的, LogicalOperationStack 应该使用async-await它不是一个bug。

我已经联系了微软的相关开发人员,他的回答如下:

我没有意识到这一点,但它似乎确实破了 。写入时复制逻辑应该表现得就像我们在进入方法时真正创建了ExecutionContext的副本一样。但是,复制ExecutionContext会创建一个CorrelationManager上下文的深层副本,因为它在CallContext.Clone()是特殊的。我们不会在写时复制逻辑中考虑到这一点。“

此外,他建议使用.Net 4.6中添加的新System.Threading.AsyncLocal类,而不应该正确处理该问题。

所以,我继续使用VS2015 RC和.Net 4.6在AsyncLocal而不是LogicalOperationStack之上实现了LogicalFlow

 public static class LogicalFlow { private static AsyncLocal _asyncLogicalOperationStack = new AsyncLocal(); private static Stack AsyncLogicalOperationStack { get { if (_asyncLogicalOperationStack.Value == null) { _asyncLogicalOperationStack.Value = new Stack(); } return _asyncLogicalOperationStack.Value; } } public static Guid CurrentOperationId => AsyncLogicalOperationStack.Count > 0 ? (Guid)AsyncLogicalOperationStack.Peek() : Guid.Empty; public static IDisposable StartScope() { AsyncLogicalOperationStack.Push(Guid.NewGuid()); return new Stopper(); } private static void StopScope() => AsyncLogicalOperationStack.Pop(); } 

并且相同测试的输出确实应该是:

 00000000-0000-0000-0000-000000000000 ae90c3e3-c801-4bc8-bc34-9bccfc2b692a ae90c3e3-c801-4bc8-bc34-9bccfc2b692a ae90c3e3-c801-4bc8-bc34-9bccfc2b692a 00000000-0000-0000-0000-000000000000 

如果你仍然对此感兴趣,我相信这是他们如何流动LogicalOperationStack的一个错误,我认为报告它是一个好主意。

它们通过深度复制(与通过CallContext.LogicalSetData/LogicalGetData存储的其他数据不同,只执行浅拷贝) 对LogicalCallContext.Clone LogicalOperationStack堆栈进行特殊处理。

每次调用ExecutionContext.CreateCopyExecutionContext.CreateMutableCopy以流动ExecutionContext.CreateMutableCopy都会调用此LogicalCallContext.Clone

基于你的代码,我做了一个小实验,为LogicalCallContext "System.Diagnostics.Trace.CorrelationManagerSlot"插槽提供了我自己的可变堆栈,以查看它实际被克隆的时间和次数。

代码:

 using System; using System.Collections; using System.Diagnostics; using System.Linq; using System.Runtime.Remoting.Messaging; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication { class Program { static readonly string CorrelationManagerSlot = "System.Diagnostics.Trace.CorrelationManagerSlot"; public static void ShowCorrelationManagerStack(object where) { object top = "null"; var stack = (MyStack)CallContext.LogicalGetData(CorrelationManagerSlot); if (stack.Count > 0) top = stack.Peek(); Console.WriteLine("{0}: MyStack Id={1}, Count={2}, on thread {3}, top: {4}", where, stack.Id, stack.Count, Environment.CurrentManagedThreadId, top); } private static void Main() { CallContext.LogicalSetData(CorrelationManagerSlot, new MyStack()); OuterOperationAsync().Wait(); Console.ReadLine(); } private static async Task OuterOperationAsync() { ShowCorrelationManagerStack(1.1); using (LogicalFlow.StartScope()) { ShowCorrelationManagerStack(1.2); Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); await InnerOperationAsync(); ShowCorrelationManagerStack(1.3); Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); await InnerOperationAsync(); ShowCorrelationManagerStack(1.4); Console.WriteLine("\t" + LogicalFlow.CurrentOperationId); } ShowCorrelationManagerStack(1.5); } private static async Task InnerOperationAsync() { ShowCorrelationManagerStack(2.1); using (LogicalFlow.StartScope()) { ShowCorrelationManagerStack(2.2); await Task.Delay(100); ShowCorrelationManagerStack(2.3); } ShowCorrelationManagerStack(2.4); } } public class MyStack : Stack, ICloneable { public static int s_Id = 0; public int Id { get; private set; } object ICloneable.Clone() { var cloneId = Interlocked.Increment(ref s_Id); ; Console.WriteLine("Cloning MyStack Id={0} into {1} on thread {2}", this.Id, cloneId, Environment.CurrentManagedThreadId); var clone = new MyStack(); clone.Id = cloneId; foreach (var item in this.ToArray().Reverse()) clone.Push(item); return clone; } } public static class LogicalFlow { public static Guid CurrentOperationId { get { return Trace.CorrelationManager.LogicalOperationStack.Count > 0 ? (Guid)Trace.CorrelationManager.LogicalOperationStack.Peek() : Guid.Empty; } } public static IDisposable StartScope() { Program.ShowCorrelationManagerStack("Before StartLogicalOperation"); Trace.CorrelationManager.StartLogicalOperation(); Program.ShowCorrelationManagerStack("After StartLogicalOperation"); return new Stopper(); } private static void StopScope() { Program.ShowCorrelationManagerStack("Before StopLogicalOperation"); Trace.CorrelationManager.StopLogicalOperation(); Program.ShowCorrelationManagerStack("After StopLogicalOperation"); } private class Stopper : IDisposable { private bool _isDisposed; public void Dispose() { if (!_isDisposed) { StopScope(); _isDisposed = true; } } } } } 

结果非常令人惊讶。 尽管此异步工作流中只涉及两个线程,但堆栈最多可克隆4次。 问题是,匹配的Stack.PushStack.Pop操作(由StartLogicalOperation / StopLogicalOperation )在堆栈的不同的,不匹配的克隆上运行,从而使“逻辑”堆栈失去平衡。 这就是bug存在的地方。

这确实使得LogicalOperationStack在异步调用中完全无法使用,即使没有并发的任务分支。

更新后 ,我还对同步调用的行为方式进行了一些研究,以解决这些问题 :

同意,而不是欺骗。 您是否检查它是否在同一个线程上按预期工作,例如,如果用Task.Delay(100)替换等待Task.Delay(100).Wait()? – Noseratio 2月27日21:00

@Noseratio是的。 它当然有效,因为只有一个线程(因此只有一个CallContext)。 这就好像该方法不是一样的异步。 – i3arnon 2月27日21:01

单线程并不意味着单个CallContext 。 即使对于同一个线程上的同步延续,也可以克隆执行上下文(及其内部LogicalCallContext )。 例如,使用上面的代码:

 private static void Main() { CallContext.LogicalSetData(CorrelationManagerSlot, new MyStack()); ShowCorrelationManagerStack(0.1); CallContext.LogicalSetData("slot1", "value1"); Console.WriteLine(CallContext.LogicalGetData("slot1")); Task.FromResult(0).ContinueWith(t => { ShowCorrelationManagerStack(0.2); CallContext.LogicalSetData("slot1", "value2"); Console.WriteLine(CallContext.LogicalGetData("slot1")); }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); ShowCorrelationManagerStack(0.3); Console.WriteLine(CallContext.LogicalGetData("slot1")); // ... } 

输出(注意我们如何失去"value2" ):

 0.1:MyStack Id = 0,Count = 0,在线程9上,顶部:
值1
在线程9上将MyStack Id = 0克隆为1
 0.2:MyStack Id = 1,Count = 0,在主题9上,顶部:
值2
 0.3:MyStack Id = 0,Count = 0,在主题9上,顶部:
值1

此处和Web上提到的解决方案之一是在上下文中调用LogicalSetData:

 CallContext.LogicalSetData("one", null); Trace.CorrelationManager.StartLogicalOperation(); 

但事实上,只需阅读当前的执行上下文就足够了:

 var context = Thread.CurrentThread.ExecutionContext; Trace.CorrelationManager.StartLogicalOperation();