[英]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 theExecutionContext
would have created a deep copy of theCorrelationManager
context, as it's special-cased inCallContext.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.Clone
对LogicalOperationStack
的堆栈进行特殊处理。
This LogicalCallContext.Clone
is called every time ExecutionContext.CreateCopy
or ExecutionContext.CreateMutableCopy
is called to flow the ExecutionContext
. 这LogicalCallContext.Clone
被称为每次ExecutionContext.CreateCopy
或ExecutionContext.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.Push
和Stack.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.