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

C# LogicalOperationStack与.Net 4.5中的异步不兼容吗,c#,.net,async-await,.net-4.5,C#,.net,Async Await,.net 4.5,Trace.CorrelationManager.LogicalOperationStack允许在最常见的情况是日志记录(NDC)的情况下使用嵌套的逻辑操作标识符。它是否仍然可以使用异步等待 下面是一个使用LogicalFlow的简单示例,它是我在LogicalOperationStack上的简单包装器: private static void Main() => OuterOperationAsync().GetAwaiter().GetResult(); private static

Trace.CorrelationManager.LogicalOperationStack
允许在最常见的情况是日志记录(NDC)的情况下使用嵌套的逻辑操作标识符。它是否仍然可以使用
异步等待

下面是一个使用
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);
    }
}
逻辑流

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
(调用时等待每个
异步
操作,并且不使用组合符,例如
任务。whalll

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

在添加到现有堆栈之前,只需在
CallContext
中设置anything即可显示这一点。例如,如果我们将
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
。完整的实施可作为此问题的答案:

那么,
LogicalOperationStack
是否应该与
async
一起工作,而这只是一个bug?
LogicalOperationStack
是否不适用于
async
世界?还是我遗漏了什么



更新:使用
任务.延迟
显然令人困惑,因为它使用了
系统.线程.计时器
。使用
wait Task.Yield()而不是
等待任务。延迟(100)
使示例更容易理解。

如果您仍然对此感兴趣,我相信这是它们如何流动的一个缺陷
LogicalOperationStack
,我认为报告它是一个好主意

它们对
LogicalOperationStack
的堆栈进行特殊处理,方法是执行深度复制(与通过
CallContext.LogicalSetData/LogicalGetData
存储的其他数据不同,后者只执行浅层复制)

每次调用
ExecutionContext.CreateCopy
ExecutionContext.CreateMutableCopy
时,都会调用此
LogicalCallContext.Clone
,以使
ExecutionContext
流动

基于您的代码,我做了一个小实验,为
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.Push
Stack.Pop
操作(称为
StartLogicalOperation
/
StopLogicalOperation
)对堆栈的不同的、不匹配的克隆进行操作,从而使“逻辑”堆栈失去平衡。这就是臭虫的所在

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

更新了,我还对它在同步调用中的行为做了一些研究,以解决以下问题:


同意,不是傻瓜。您是否检查了它是否在同一台机器上按预期工作 线程,例如,如果将wait Task.Delay(100)替换为 任务。延迟(100)。等待()2月27日21:00

@是的。它当然可以工作,因为只有一个线程(因此只有一个CallContext)。就好像这个方法不是 首先是异步的12月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"));

    // ...
}
输出(请注意我们如何丢失“值2”
):

0.1:MyStack Id=0,Count=0,在线程9上,顶部: 价值1 正在将MyStack Id=0克隆到线程9上的1中 0.2:MyStack Id=1,Count=0,在线程9上,顶部: 价值2 0.3:MyStack Id=0,Count=0,在线程9上,顶部: 价值1
是的,
LogicalOperationStack
应该与
async await
一起工作,但它不是一个bug

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

我没有意识到这一点,但它看起来确实坏了。写时复制逻辑的行为应该与我们在进入方法时真正创建了
ExecutionContext
的副本完全相同。但是,复制
ExecutionContext
会创建
CorrelationManager
上下文的深度副本,因为它在
CallContext.Clone()中是特殊情况。我们在写拷贝逻辑中没有考虑到这一点。”

此外,他建议改用.NET4.6中添加的新类,该类应能正确处理该问题

所以,我去了 0.1: MyStack Id=0, Count=0, on thread 9, top: value1 Cloning MyStack Id=0 into 1 on thread 9 0.2: MyStack Id=1, Count=0, on thread 9, top: value2 0.3: MyStack Id=0, Count=0, on thread 9, top: value1
public static class LogicalFlow
{
    private static AsyncLocal<Stack> _asyncLogicalOperationStack = new AsyncLocal<Stack>();

    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
CallContext.LogicalSetData("one", null);
Trace.CorrelationManager.StartLogicalOperation();
var context = Thread.CurrentThread.ExecutionContext;
Trace.CorrelationManager.StartLogicalOperation();