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。完整的实现方案可以作为对此问题的答案:Tracking c#/.NET tasks flow

那么,LogicalOperationStack是否可以与async一起使用,这仅仅是一个错误? LogicalOperationStack是否仅适用于async世界?还是我错过了什么?

更新:使用Task.Delay显然会造成混淆,因为它使用captures the System.Threading.Timer internally,即ojit_a。使用ExecutionContext而不是await Task.Yield();使示例更易于理解。

最佳答案

是的,LogicalOperationStack应该可以与async-await一起使用,但它不是一个Bug。

我已经与Microsoft的相关开发人员联系,他的答复是:



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

因此,我继续使用VS2015 RC和.Net 4.6在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();
}

相同测试的输出确实应为:
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

10-08 05:20