是LogicalOperationStack与异步的净4.5不兼容不兼容、LogicalOperationStack

2023-09-03 11:14:04 作者:绝望的歌

Trace.CorrelationManager.LogicalOperationStack 使得具有嵌套逻辑操作标识,其中最常见的情况是日志(NDC)。如果它仍与工作异步等待

下面是一个使用一个简单的例子 LogicalFlow 这是我简单的包装在 LogicalOperationStack

 私有静态无效的主要()
{
    OuterOperationAsync()等待()。
}

私有静态异步任务OuterOperationAsync()
{
    Console.WriteLine(LogicalFlow.CurrentOperationId);
    使用(LogicalFlow.StartScope())
    {
        Console.WriteLine(\ t+ LogicalFlow.CurrentOperationId);
        等待InnerOperationAsync();
        Console.WriteLine(\ t+ LogicalFlow.CurrentOperationId);
        等待InnerOperationAsync();
        Console.WriteLine(\ t+ LogicalFlow.CurrentOperationId);
    }
    Console.WriteLine(LogicalFlow.CurrentOperationId);
}

私有静态异步任务InnerOperationAsync()
{
    使用(LogicalFlow.StartScope())
    {
        等待Task.Delay(100);
    }
}
 

LogicalFlow

 公共静态类LogicalFlow
{
    公共静态的Guid CurrentOperationId
    {
        得到
        {
            返回Trace.CorrelationManager.LogicalOperationStack.Count> 0
                ? (GUID)Trace.CorrelationManager.LogicalOperationStack.Peek()
                :Guid.Empty;
        }
    }

    公共静态IDisposable的StartScope()
    {
        Trace.CorrelationManager.StartLogicalOperation();
        返回新制动器();
    }

    私有静态无效StopScope()
    {
        Trace.CorrelationManager.StopLogicalOperation();
    }

    私有类塞:IDisposable的
    {
        私人布尔_isDisposed;
        公共无效的Dispose()
        {
            如果(!_isDisposed)
            {
                StopScope();
                _isDisposed = TRUE;
            }
        }
    }
}
 
JAVA五种IO模型

输出:

  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 正在使用堆栈这是不是线程安全的,这就是为什么输出错误。不过,虽然这是在一般的事实,在这种情况下的还有永远不是访问 LogicalOperationStack 同时(每异步操作等待调用的时候,并没有使用组合程序,如 Task.WhenAll

问题是, LogicalOperationStack 存储在 CallContext中其中有一个副本上写的行为。这意味着,只要你不明确设置一些在 CallContext中(而你不知道,当您添加到现有的堆叠与 StartLogicalOperation )您使用的是父上下文,而不是你自己的。

这可以添加到现有的堆栈前显示通过简单的设置什么到 CallContext中。例如,如果我们改变 StartScope 来这样的:

 公共静态IDisposable的StartScope()
{
    CallContext.LogicalSetData(酒吧,嫩);
    Trace.CorrelationManager.StartLogicalOperation();
    返回新制动器();
}
 

的输出是:

  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 ,因为它是两个线程安全的,因为它是不可变的当你调用流行你会得到一个新的 ImmutableStack ,你再需要设置回 CallContext中。一个完整的实现是可以作为这个问题的答案:跟踪C#/。NET的任务流程 的

所以,应该 LogicalOperationStack 工作异步,它只是一个错误?为 LogicalOperationStack 只是不意味着对异步世界吗?还是我失去了一些东西?

更新:使用 Task.Delay 显然是令人困惑,因为它使用 System.Threading.Timer 这捕捉执行上下文内部。使用等待Task.Yield(); 而不是等待Task.Delay(100); 使得例子更容易理解

解决方案

LogicalOperationStack 的应该的与工作异步-await 和是的是它不是一个错误。

我已经联系在微软相关的开发商和他的回答是这样的:

  

我不知道这一点,但它似乎打破的副本上写逻辑应该表现得就好像我们真的很创建的执行上下文的进入方法,但是,复制执行上下文将创造的深层副本 CorrelationManager 背景下,作为它的特例,在 CallContext.Clone()。我们不考虑到这一点,在副本上写逻辑。

此外,他建议在使用新的 System.Threading.AsyncLocal< T> 类的.Net 4.6,而不是加入其中要正确处理这个问题。

于是,我继续实施 LogicalFlow 上的顶部 AsyncLocal 而不是 LogicalOperationStack 用VS2015 RC和.Net 4.6:

 公共静态类LogicalFlow
{
    私有静态AsyncLocal<栈> _asyncLogicalOperationStack;
    私有静态堆栈AsyncLogicalOperationStack
    {
        得到
        {
            如果(_asyncLogicalOperationStack == NULL)
            {
                _asyncLogicalOperationStack =新AsyncLocal<栈> {值=新栈()};
            }

            返回_asyncLogicalOperationStack.Value;
        }
    }

    公共静态的Guid CurrentOperationId
    {
        得到
        {
            返回AsyncLogicalOperationStack.Count> 0
                ? (GUID)AsyncLogicalOperationStack.Peek()
                :Guid.Empty;
        }
    }

    公共静态IDisposable的StartScope()
    {
        AsyncLogicalOperationStack.Push(Guid.NewGuid());
        返回新制动器();
    }

    私有静态无效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
 

Trace.CorrelationManager.LogicalOperationStack enables having nested logical operation identifiers where the most common case is logging (NDC). Should it still work with async-await?

Here's a simple example using LogicalFlow which is my simple wrapper over the LogicalOperationStack:

private static void Main()
{
    OuterOperationAsync().Wait();
}

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
    {
        get
        {
            return 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 (i.e. 00000000-0000-0000-0000-000000000000) and the inner lines should show the same Guid value.

You might say that LogicalOperationStack is using a Stack which is not thread-safe and that's why the output is wrong. 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)

The issue is that LogicalOperationStack is stored in the CallContext which has a copy-on-write behavior. 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.

This can be shown by simply setting anything into the CallContext before adding to the existing stack. For example if we changed StartScope to this:

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. A full implementation is available as an answer to this question: Tracking c#/.NET tasks flow

So, should LogicalOperationStack work with async and it's just a bug? Is LogicalOperationStack just not meant for the async world? Or am I missing something?

Update: Using Task.Delay is apparently confusing as it uses System.Threading.Timer which captures the ExecutionContext internally. Using await Task.Yield(); instead of 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.

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."

Moreover, he recommended using the new System.Threading.AsyncLocal<T> class added in .Net 4.6 instead which should handle that issue correctly.

So, I went ahead and implemented LogicalFlow on top of an AsyncLocal instead of the LogicalOperationStack using VS2015 RC and .Net 4.6:

public static class LogicalFlow
{
    private static AsyncLocal<Stack> _asyncLogicalOperationStack;
    private static Stack AsyncLogicalOperationStack
    {
        get
        {
            if (_asyncLogicalOperationStack == null)
            {
                _asyncLogicalOperationStack = new AsyncLocal<Stack> {Value = new Stack()};
            }

            return _asyncLogicalOperationStack.Value;
        }
    }

    public static Guid CurrentOperationId
    {
        get
        {
            return 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

 
精彩推荐