简体   繁体   English

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

[英]Is LogicalOperationStack incompatible with async in .Net 4.5

Trace.CorrelationManager.LogicalOperationStack enables having nested logical operation identifiers where the most common case is logging (NDC). Trace.CorrelationManager.LogicalOperationStack允许嵌套逻辑操作标识符,其中最常见的情况是日志记录(NDC)。 Should it still work with async-await ? 是否仍然可以使用async-await

Here's a simple example using LogicalFlow which is my simple wrapper over the LogicalOperationStack : 这是一个使用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 : 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;
            }
        }
    }
}

Output: 输出:

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

The specific values don't really matter, but as I understand it both the outer lines should show Guid.Empty (ie 00000000-0000-0000-0000-000000000000 ) and the inner lines should show the same Guid value. 具体值并不重要,但据我所知,外线应显示Guid.Empty (即00000000-0000-0000-0000-000000000000 ),内线应显示相同的Guid值。

You might say that LogicalOperationStack is using a Stack which is not thread-safe and that's why the output is wrong. 您可能会说LogicalOperationStack正在使用不是线程安全的Stack ,这就是输出错误的原因。 But while that's true in general, in this case there's never more than a single thread accessing the LogicalOperationStack at the same time (every async operation is awaited when called and no use of combinators such as Task.WhenAll ) 但总的来说,这是正确的,在这种情况下,同时访问LogicalOperationStack线程永远不会超过一个 (调用时等待每个async操作,并且不使用任何组合器,如Task.WhenAll

The issue is that LogicalOperationStack is stored in the CallContext which has a copy-on-write behavior. 问题是LogicalOperationStack存储在具有写时复制行为的CallContext中。 That means that as long as you don't explicitly set something in the CallContext (and you don't when you add to an existing stack with StartLogicalOperation ) you're using the parent context and not your own. 这意味着只要您没有在CallContext显式设置某些内容(当您使用StartLogicalOperation添加到现有堆栈时就没有),您将使用父上下文而不是您自己的上下文。

This can be shown by simply setting anything into the CallContext before adding to the existing stack. 这可以通过在添加到现有堆栈之前简单地将任何内容设置到CallContext来显示。 For example if we changed StartScope to this: 例如,如果我们将StartScope更改为:

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

The output is: 输出是:

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

Note: I'm not suggesting anyone actually do this. 注意: 我不是建议任何人真正这样做。 The real practical solution would be to use an ImmutableStack instead of the LogicalOperationStack as it's both thread-safe and since it's immutable when you call Pop you get back a new ImmutableStack that you then need to set back into the CallContext . 真正实用的解决方案是使用ImmutableStack而不是LogicalOperationStack因为它既是线程安全的,因为当你调用Pop时它是不可变的,你会得到一个新的ImmutableStack ,然后你需要将它设置回CallContext A full implementation is available as an answer to this question: Tracking c#/.NET tasks flow 完整的实现可以作为这个问题的答案: 跟踪c#/ .NET任务流程

So, should LogicalOperationStack work with async and it's just a bug? 那么, LogicalOperationStack应该使用async并且它只是一个错误吗? Is LogicalOperationStack just not meant for the async world? LogicalOperationStack是不是意味着async世界? Or am I missing something? 或者我错过了什么?


Update : Using Task.Delay is apparently confusing as it uses System.Threading.Timer which captures the ExecutionContext internally . 更新 :使用Task.Delay显然令人困惑,因为它使用System.Threading.Timer 在内部捕获ExecutionContext Using await Task.Yield(); 使用await Task.Yield(); instead of await Task.Delay(100); 而不是await Task.Delay(100); makes the example easier to understand. 使示例更容易理解。

Yes, LogicalOperationStack should work with async-await and it is a bug that it doesn't. 是的, LogicalOperationStack 应该async-await它不是一个bug。

I've contacted the relevant developer at Microsoft and his response was this: 我已经联系了微软的相关开发人员,他的回答如下:

" I wasn't aware of this, but it does seem broken . The copy-on-write logic is supposed to behave exactly as if we'd really created a copy of the ExecutionContext on entry into the method. However, copying the ExecutionContext would have created a deep copy of the CorrelationManager context, as it's special-cased in CallContext.Clone() . We don't take that into account in the copy-on-write logic." 我没有意识到这一点,但它似乎确实破了 。写入时复制逻辑应该表现得就像我们在进入方法时真正创建了ExecutionContext的副本一样。但是,复制ExecutionContext会创建一个CorrelationManager上下文的深层副本,因为它在CallContext.Clone()是特殊的。我们不会在写时复制逻辑中考虑到这一点。“

Moreover, he recommended using the new System.Threading.AsyncLocal<T> class added in .Net 4.6 instead which should handle that issue correctly. 此外,他建议使用.Net 4.6中添加的新System.Threading.AsyncLocal<T>类,而不应该正确处理该问题。

So, I went ahead and implemented LogicalFlow on top of an AsyncLocal instead of the LogicalOperationStack using VS2015 RC and .Net 4.6: 所以,我继续使用VS2015 RC和.Net 4.6在AsyncLocal而不是LogicalOperationStack之上实现了LogicalFlow

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();
}

And the output for the same test is indeed as it should be: 并且相同测试的输出确实应该是:

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

If you're still interested in this, I believe it's a bug in how they flow LogicalOperationStack and I think it's a good idea to report it. 如果您仍然对此感兴趣,我相信它们是如何流动LogicalOperationStack ,我认为报告它是一个好主意。

They give special treatment to LogicalOperationStack 's stack here in LogicalCallContext.Clone , by doing a deep copy (unlike with other data stored via CallContext.LogicalSetData/LogicalGetData , on which only a shallow copy is performed). 它们通过执行深度复制(与通过CallContext.LogicalSetData/LogicalGetData存储的其他数据不同,只执行浅复制), LogicalCallContext.CloneLogicalOperationStack的堆栈进行特殊处理。

This LogicalCallContext.Clone is called every time ExecutionContext.CreateCopy or ExecutionContext.CreateMutableCopy is called to flow the ExecutionContext . LogicalCallContext.Clone被称为每次ExecutionContext.CreateCopyExecutionContext.CreateMutableCopy被称为流动的ExecutionContext

Based on your code, I did a little experiment by providing my own mutable stack for "System.Diagnostics.Trace.CorrelationManagerSlot" slot in LogicalCallContext , to see when and how many times it actually gets cloned. 基于你的代码,我做了一个小实验,为LogicalCallContext "System.Diagnostics.Trace.CorrelationManagerSlot"插槽提供了我自己的可变堆栈,以查看它实际被克隆的时间和次数。

The code: 代码:

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;
                }
            }
        }
    }
}

The result is quite surprising. 结果非常令人惊讶。 Even though there're only two threads involved in this async workflow, the stack gets cloned as many as 4 times. 即使此异步工作流中只涉及两个线程,堆栈也会被克隆多达4次。 And the problem is, the matching Stack.Push and Stack.Pop operations (called by StartLogicalOperation / StopLogicalOperation ) operate on the different, non-matching clones of the stack, thus disbalancing the "logical" stack. 问题是,匹配的Stack.PushStack.Pop操作(由StartLogicalOperation / StopLogicalOperation )在堆栈的不同的,不匹配的克隆上运行,从而使“逻辑”堆栈失去平衡。 That's where the bug lays in. 这就是bug存在的地方。

This indeed makes LogicalOperationStack totally unusable across async calls, even though there's no concurrent forks of tasks. 这确实使得LogicalOperationStack在异步调用中完全无法使用,即使没有并发的任务分支。

Updated , I also did a little research about how it may behave for synchronous calls, to address these comments : 更新后 ,我还对同步调用的行为方式进行了一些研究,以解决这些问题

Agreed, not a dupe. 同意,而不是欺骗。 Did you check if it works as expected on the same thread, eg if you replace await Task.Delay(100) with Task.Delay(100).Wait()? 您是否在同一个线程上检查它是否按预期工作,例如,如果用Task.Delay(100)替换等待Task.Delay(100).Wait()? – Noseratio Feb 27 at 21:00 - Noseratio 2月27日21:00

@Noseratio yes. @Noseratio是的。 It works of course, because there's only a single thread (and so a single CallContext). 它当然有效,因为只有一个线程(因此只有一个CallContext)。 It's as if the method wasn't async to begin with. 这就好像该方法不是一开始就是异步的。 – i3arnon Feb 27 at 21:01 - i3arnon 2月27日21:01

Single thread doesn't mean single CallContext . 单线程并不意味着单个CallContext Even for synchronous continuations on the same single thread the execution context (and its inner LogicalCallContext ) can get cloned. 即使对于同一个线程上的同步延续,也可以克隆执行上下文(及其内部LogicalCallContext )。 Example, using the above code: 例如,使用上面的代码:

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"));

    // ...
}

Output (note how we lose "value2" ): 输出(注意我们如何失去"value2" ):

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

One of the solutions mentioned here and on the web is to call LogicalSetData on context: 此处和Web上提到的解决方案之一是在上下文中调用LogicalSetData:

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

But in fact, just reading current execution context is enough: 但事实上,只需阅读当前的执行上下文就足够了:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM