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

时间:2015-02-27 16:12:41

标签: c# .net async-await .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),内线应显示相同的{{1} }值。

您可能会说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。完整实施可用作此问题的答案:Tracking c#/.NET tasks flow

那么,CallContext是否应该使用LogicalOperationStack并且它只是一个错误? async是否仅适用于LogicalOperationStack世界?或者我错过了什么?


更新:使用async显然令人困惑,因为它使用了Task.Delay System.Threading.Timer。使用await Task.Yield();代替await Task.Delay(100);可以使示例更易于理解。

3 个答案:

答案 0 :(得分:12)

是的,LogicalOperationStack async-await合作,而 是的错误。

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

  

我没有意识到这一点,但它看起来似乎已经破碎了。写入时复制逻辑应该表现得就像我们真的创建了{{{ 1}}在进入方法时。但是,复制ExecutionContext会创建ExecutionContext上下文的深层副本,因为它在CorrelationManager中是特殊的。我们不会在写时复制逻辑中考虑到这一点。“

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

所以,我继续使用VS2015 RC和.Net 4.6在CallContext.Clone()而不是LogicalFlow之上实施AsyncLocal

LogicalOperationStack

同一测试的输出确实应该是:

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

答案 1 :(得分:6)

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

他们对LogicalOperationStack的堆栈here in LogicalCallContext.Clone进行特殊处理,通过执行深层复制(与通过CallContext.LogicalSetData/LogicalGetData存储的其他数据不同,其中只有浅层副本执行)。

每次调用LogicalCallContext.CloneExecutionContext.CreateCopyExecutionContext.CreateMutableCopy时都会调用此ExecutionContext

根据您的代码,我通过为"System.Diagnostics.Trace.CorrelationManagerSlot"中的LogicalCallContext插槽提供我自己的可变堆栈进行了一些实验,以查看实际克隆的时间和次数。

代码:

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调用)在堆栈的不同的,不匹配的克隆上运行,从而破坏了&#34;逻辑&#34;堆。那就是虫子所处的位置。

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

已更新,我还对同步调用的行为方式进行了一些研究,以解决these comments

  

同意,而不是欺骗。你有没有检查它是否按预期工作   线程,例如如果你替换等待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, 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

答案 2 :(得分:0)

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

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

但是实际上,仅读取当前执行上下文就足够了:

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