c# - Es LogicalOperationStack incompatible con async en.Net 4.5
async-await .net-4.5 (3)
Trace.CorrelationManager.LogicalOperationStack
permite tener identificadores de operaciones lógicas anidadas donde el caso más común es el registro (NDC). ¿Debería seguir funcionando con async-await
?
Aquí hay un ejemplo simple utilizando LogicalFlow
que es mi envoltorio simple sobre 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;
}
}
}
}
Salida:
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
Los valores específicos realmente no importan, pero como lo entiendo, tanto las líneas externas deben mostrar Guid.Empty
(es decir, 00000000-0000-0000-0000-000000000000
) y las líneas internas deben mostrar el mismo valor de Guid
.
Podría decir que LogicalOperationStack
está utilizando una Stack
que no es segura para subprocesos y es por eso que la salida es incorrecta. Pero aunque eso es cierto en general, en este caso , nunca hay más de un solo hilo que accede a LogicalOperationStack
al mismo tiempo (se espera cada operación async
cuando se llama y no se utilizan combinadores como Task.WhenAll
)
El problema es que LogicalOperationStack
se almacena en el CallContext
que tiene un comportamiento de copia en escritura. Eso significa que siempre y cuando no establezcas explícitamente algo en el CallContext
(y no lo hagas cuando agregas a una pila existente con StartLogicalOperation
) estás usando el contexto principal y no el tuyo.
Esto se puede mostrar simplemente configurando cualquier cosa en el CallContext
antes de agregarlo a la pila existente. Por ejemplo, si cambiamos StartScope
a esto:
public static IDisposable StartScope()
{
CallContext.LogicalSetData("Bar", "Arnon");
Trace.CorrelationManager.StartLogicalOperation();
return new Stopper();
}
La salida es:
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
Nota: no estoy sugiriendo que alguien realmente haga esto. La solución práctica real sería usar un ImmutableStack
lugar del LogicalOperationStack
ya que es seguro para subprocesos y, dado que es inmutable, cuando llamas a Pop
, recibes un nuevo ImmutableStack
que luego debes volver a establecer en CallContext
. Una implementación completa está disponible como respuesta a esta pregunta: Seguimiento de flujo de tareas c # / .NET
Entonces, ¿debería funcionar LogicalOperationStack
con async
y es solo un error? ¿ LogicalOperationStack
no está destinado al mundo async
? ¿O me estoy perdiendo algo?
Actualización : el uso de Task.Delay
es aparentemente confuso ya que usa System.Threading.Timer
que captura el ExecutionContext
internamente . Utilizando await Task.Yield();
En lugar de await Task.Delay(100);
hace que el ejemplo sea más fácil de entender.
Sí, LogicalOperationStack
debería funcionar con async-await
y es un error que no funciona.
Me puse en contacto con el desarrollador relevante de Microsoft y su respuesta fue la siguiente:
" No estaba al tanto de esto, pero parece estar roto . Se supone que la lógica de copia en escritura se comporta exactamente como si realmente hubiéramos creado una copia de
ExecutionContext
al ingresar al método. Sin embargo, copiando elExecutionContext
habría creado una copia en profundidad del contexto deCorrelationManager
, ya que está especial en el contexto deCallContext.Clone()
. No tenemos eso en cuenta en la lógica de copia en escritura ".
Además, recomendó usar la nueva clase System.Threading.AsyncLocal<T>
agregada en .Net 4.6 en lugar de eso, que debería manejar ese problema correctamente.
Entonces, seguí adelante e implementé LogicalFlow
sobre LogicalFlow
en lugar de LogicalOperationStack
usando VS2015 RC y .Net 4.6:
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();
}
Y la salida para la misma prueba es, en efecto, como debería ser:
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
Si aún estás interesado en esto, creo que es un error en la forma en que fluyen LogicalOperationStack
y creo que es una buena idea reportarlo.
Le dan un tratamiento especial a la pila de LogicalCallContext.Clone
aquí en LogicalCallContext.Clone
, haciendo una copia profunda (a diferencia de otros datos almacenados a través de CallContext.LogicalSetData/LogicalGetData
, en la que solo se realiza una copia superficial).
Este LogicalCallContext.Clone
se llama cada vez que se llama a ExecutionContext.CreateCopy
o ExecutionContext.CreateMutableCopy
para fluir el ExecutionContext
.
Basado en su código, hice un pequeño experimento al proporcionar mi propia pila mutable para la ranura "System.Diagnostics.Trace.CorrelationManagerSlot"
en LogicalCallContext
, para ver cuándo y cuántas veces realmente se clona.
El código:
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;
}
}
}
}
}
El resultado es bastante sorprendente. Aunque solo hay dos subprocesos involucrados en este flujo de trabajo asíncrono, la pila se clona hasta 4 veces. Y el problema es que las operaciones Stack.Push
y Stack.Pop
(llamadas por StartLogicalOperation
/ StopLogicalOperation
) operan en los diferentes clones no coincidentes de la pila, lo que desestabiliza la pila "lógica". Ahí es donde se encuentra el error.
De hecho, esto hace que LogicalOperationStack
sea totalmente inutilizable en las llamadas asíncronas, aunque no haya forks concurrentes de tareas.
Actualizado , también hice una pequeña investigación sobre cómo puede comportarse en las llamadas sincrónicas, para abordar estos comentarios :
De acuerdo, no un dupe. ¿Comprobó si funciona como se esperaba en el mismo hilo, por ejemplo, si reemplaza a esperar Task.Delay (100) con Task.Delay (100) .Wait ()? - Noseratio 27 de febrero a las 21:00.
@Noseratio si. Funciona, por supuesto, porque solo hay un solo hilo (y por lo tanto un solo CallContext). Es como si el método no fuera asincrónico para empezar. - i3arnon 27 de febrero a las 21:01
Un solo hilo no significa solo CallContext
. Incluso para las continuaciones síncronas en el mismo subproceso, el contexto de ejecución (y su LogicalCallContext
interno) se puede clonar. Ejemplo, usando el código anterior:
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"));
// ...
}
Salida (note como perdemos "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
Una de las soluciones mencionadas aquí y en la web es llamar a LogicalSetData en contexto:
CallContext.LogicalSetData("one", null);
Trace.CorrelationManager.StartLogicalOperation();
Pero de hecho, basta con leer el contexto de ejecución actual:
var context = Thread.CurrentThread.ExecutionContext;
Trace.CorrelationManager.StartLogicalOperation();