Este artículo proviene de un motor de traducción automática.
Programación asincrónica
Seguimiento asincrónico de la cadena de causalidad
Descargar el ejemplo de código
Con el advenimiento de C# 5, Visual Basic .NET 11, Microsoft .NET Framework 4.5 y aplicaciones de .NET para Windows Store, la experiencia de programación asincrónica ha racionalizado considerablemente.Nuevo async y esperan palabras clave (Async y aguarda en Visual Basic) permiten a los desarrolladores a mantener la misma abstracción estaban acostumbrados al escribir el código síncrono.
Un gran esfuerzo se puso en Visual Studio 2012 para mejorar asincrónico de depuración con herramientas como pilas paralelas, tareas paralelas, reloj paralelo y el visualizador de simultaneidad.Sin embargo, en términos de ser a la par con el código síncrono experiencia de depuración, no estamos bastante allí todavía.
Uno de los temas más prominentes que rompe la abstracción y revela la plomería interior detrás de la fachada async/esperan es la falta de información de la pila de llamadas en el depurador.En este artículo, voy a proporcionar medios para cerrar esta brecha y mejorar la experiencia de depuración asincrónica en su aplicación .NET 4.5 o Windows Store.
¡ Vamos a arreglar en terminología esencial primero.
Definición de una pila de llamadas
Documentación de MSDN (bit.ly/Tukvkm) utilizados para definir la pila de llamadas como "la serie de llamadas a métodos lleva desde el principio del programa a la instrucción que se esté ejecutando en tiempo de ejecución". Esta noción era perfectamente válida para el modelo de programación único subproceso, sincrónico, pero ahora que el paralelismo y la asincronía están ganando impulso, taxonomía más precisa es necesario.
Con el propósito de este artículo, es importante distinguir la cadena de causalidad de la pila de retorno.Dentro del paradigma síncrono, estos dos términos son idénticos (mencionaré el caso excepcional más adelante).En código asíncrono, la definición mencionada describe una cadena de causalidad.
Por otra parte, la declaración que se esté ejecutando, cuando haya terminado, dará lugar a una serie de métodos continuar su ejecución.Esta serie constituye la pila de retorno.Por otra parte, para los lectores familiarizados con la continuación pasando estilo (Eric Lippert tiene unas fabulosas sobre este tema, a partir de bit.ly/d9V0Dc), la pila de retorno podría definirse como una serie de consecuencias que se registran para ejecutar, debe completar el método actualmente en ejecución.
En pocas palabras, la cadena de causalidad responde a la pregunta, "¿Cómo obtuve aquí?" mientras que el retorno de la pila es la respuesta, "Dónde puedo siguiente?" Por ejemplo, si tienes un bloqueo en su aplicación, podría ser capaz de averiguar qué causó el anterior, mientras que este último le permiten saber cuáles son las consecuencias.Tenga en cuenta que mientras que una cadena de causalidad siempre pistas hacia el punto de entrada del programa, la pila de retorno se corta en el punto donde no se observa el resultado de la operación asincrónica (por ejemplo, métodos void async o trabajo programado vía ThreadPool.QueueUserWorkItem).
También hay una noción de seguimiento de la pila es una copia de una pila de llamadas sincrónicas preservada para el diagnóstico; Usaré indistintamente estos dos términos.
Tenga en cuenta que hay varias hipótesis implícitas en las definiciones anteriores:
- "Llamadas al método" contemplados en la primera definición implican generalmente "métodos que no han terminado aún," que tengan el significado físico de "ser en pila" en el modelo de programación sincrónico.Sin embargo, mientras que por lo general no estamos interesados en métodos que ya han regresado, no resulta siempre posible distinguir durante la depuración asincrónica.En este caso, no hay ninguna noción física del "ser en pila" y continuaciones todos son igualmente elementos válidos de una cadena de causalidad.
- Incluso en código sincrónico, una cadena de causalidad y el retorno de la pila no siempre idénticos.Un caso particular cuando un método podría estar presente en uno, pero falta el otro, es una llamada de cola.Aunque no directamente expresable en C# y Visual Basic . net, puede ser codificado en lenguaje intermedio (IL) ("cola." prefijo) o producido por el compilador de just-in-time (JIT) (especialmente en un proceso de 64 bits).
- Por último, pero no menos importante, las cadenas de causalidad y pilas de retorno pueden ser no lineales.Es decir, en el caso más general, están dirigidos gráficos tener sentencia actual como un lavabo (gráfico de la causalidad) o fuente (gráfico de retorno).No linealidad en código asíncrono es debido a las horquillas (paralelas operaciones asincrónicas procedentes de uno) y se une a (continuación programada para ejecutarse sobre la terminación de un conjunto de operaciones asincrónicas paralelos).Con el propósito de este artículo y debido a las limitaciones de la plataforma (explicadas más adelante), voy considerar sólo las cadenas de causalidad lineal y volver a pilas, que son subconjuntos de los gráficos correspondientes.
Por suerte, si asincronía se introduce en un programa usando async y esperan palabras clave con horquillas ni se une y se esperan todos los métodos asincrónicos, la cadena de causalidad es todavía idéntica a la pila de retorno, al igual que en el código síncrono.En este caso, ambos son igualmente útiles para orientar a ti mismo en el flujo de control.
Por otra parte, las cadenas de causalidad son raramente iguales volver apilados en programas que emplea explícitamente previsto continuaciones, un ejemplo notable es tarea paralela biblioteca (TPL) flujo de datos.Esto es debido a la naturaleza de los datos que fluyen de un bloque de fuente a un bloque de destino, nunca regresar a la anterior.
Herramientas existentes
Veamos un ejemplo rápido:
static void Main()
{
OperationAsync().Wait();
}
async static Task OperationAsync()
{
await Task.Delay(1000);
Console.WriteLine("Where is my call stack?");
}
Extrapolando la abstracción de los desarrolladores se solían en depuración sincrónica, esperaría que la siguiente pila de cadena/retorno de causalidad cuando se interrumpe la ejecución en el método Console.WriteLine:
ConsoleSample.exe!ConsoleSample.Program.OperationAsync() Line 19
ConsoleSample.exe!ConsoleSample.Program.Main() Line 13
Pero si tratas de esto, usted encontrará que en la ventana pila de llamadas el método Main es falta, mientras que el seguimiento de la pila comienza directamente en el método OperationAsync precedido por [método de Async reasumir]. Pilas paralelas tiene dos métodos; sin embargo, que no se refleja que las principales llamadas OperationAsync. Tareas paralelas, no ayuda a no que "Ninguna tarea quieras."
Nota: En este punto el depurador es consciente del método principal ser parte de la pila de llamadas — usted puede haber notado que por el fondo gris detrás de la llamada a OperationAsync. El CLR y el tiempo de ejecución de Windows (WinRT) tienen que saber donde continuar la ejecución después de que devuelve el marco superior de la pila; así, de hecho almacenan pilas de retorno. En este artículo, sin embargo, voy sólo profundizar en causalidad seguimiento, dejando montones de retorno como un tema para otro artículo.
Preservar las cadenas de causalidad
De hecho, las cadenas de causalidad nunca se almacenan por el tiempo de ejecución. Incluso pilas de llamadas que ves al depurar código sincrónico son, en esencia, volver a pilas, como se acaba de decir, son necesarias para el CLR y tiempo de ejecución de Windows saber qué métodos para ejecutar después devuelve el marco superior. El tiempo de ejecución no necesita saber qué causó un método en particular ejecutar.
Para poder ver las cadenas de causalidad en vivo y post-mortem debugging, deberás conservarlos explícitamente en el camino. Probablemente, esto requeriría almacenar información de seguimiento de la pila (sincrónico) en cada punto donde está prevista la continuación y la restauración de estos datos cuando continuación empieza a ejecutar. Estos pila traza segmentos entonces podrían ser cosidos juntos para formar una cadena de causalidad.
Estamos más interesados en la transferencia de información de causalidad aguardan a través construcciones, ya que es donde se rompe la abstracción de semejanza con código síncrono. Vamos a ver cómo y Cuándo estos datos pueden ser capturados.
Como señala Stephen Toub (bit.ly/yF8eGu), siempre que FooAsync devuelve una tarea, el siguiente código:
await FooAsync();
RestOfMethod();
se transforma por el compilador a un áspero equivalente a esto:
var t = FooAsync();
var currentContext = SynchronizationContext.Current;
t.ContinueWith(delegate
{
if (currentContext == null)
RestOfMethod();
else
currentContext.Post(delegate { RestOfMethod(); }, null);
}, TaskScheduler.Current);
De mirar el código ampliado, parece que hay al menos dos puntos de extensión que permita para capturar información de causalidad: TaskScheduler y SynchronizationContext. De hecho, ambos ofrecen pares similares de métodos virtuales donde debería ser posible captar segmentos de pila de llamadas en los momentos de la derecho: QueueTask/TryDequeue en TaskScheduler y Post/OperationStarted en SynchronizationContext.
Lamentablemente, sólo se puede sustituir por defecto TaskScheduler al programar explícitamente un delegado mediante la API de TPL, tales como Task.Run, Task.ContinueWith, TaskFactory.StartNew y así sucesivamente. Esto significa que cuando está prevista la continuación fuera una tarea de ejecución, el valor predeterminado TaskScheduler estará en vigor. Así, el TaskScheduler -enfoque basado no será capaces de capturar la información necesaria.
En cuanto a SynchronizationContext, aunque es posible reemplazar la instancia predeterminada de esta clase para el subproceso actual llamando al método SynchronizationContext.SetSynchronizationContext, esto debe hacerse para cada subproceso de la aplicación. Por lo tanto, usted tendría que ser capaces de la vida de hilo de control, que no es factible si no planea implementar un grupo de subprocesos. Además, Windows Forms, Windows Presentation Foundation (WPF) y ASP.NET ofrecen sus propias implementaciones de SynchronizationContext además de SynchronizationContext.Default, que los horarios de trabajo para el grupo de subprocesos. Por lo tanto, su aplicación tendría que se comportan diferentemente dependiendo de la procedencia del subproceso en el que está trabajando.
También tenga en cuenta que cuando se espera una costumbre awaitable, es totalmente hasta la implementación si utilizar SynchronizationContext para programar una continuación.
Por suerte, hay dos puntos de extensión adecuado para nuestro escenario: suscripción a eventos TPL sin tener que modificar el código existente, u optar explícitamente en modificando ligeramente cada aguardan la expresión en la aplicación. El primer acercamiento sólo funciona en aplicaciones de escritorio .NET, mientras que el segundo puede alojar aplicaciones Windows Store. Vamos a detallar tanto en las secciones siguientes.
Introducción EventSource
.NET Framework admite el evento Tracing para Windows (ETW), habiendo definido proveedores de eventos para prácticamente todos los aspectos de la ejecución (bit.ly/VDfrtP). Particularmente, TPL dispara eventos que permiten hacer un seguimiento de la duración de la tarea. Aunque no todos estos eventos están documentados, puede obtener sus definiciones a ti mismo por adentrarse en mscorlib.dll con una herramienta como ILSpy o Reflector o leerlo en fuente de referencia de marco (bit.ly/HRU3) y buscando la clase TplEtwProvider. Por supuesto, la renuncia de reflexión generalmente se aplica: El API no documentadas, no hay ninguna garantía de que el comportamiento observado empíricamente se conservarán en la próxima versión.
TplEtwProvider hereda de System.Diagnostics.Tracing.EventSource, que fue introducida en el .NET Framework 4.5 y es ahora un método recomendado para disparar eventos ETW en su aplicación (anteriormente se tenía que lidiar con manual ETW manifiestan generación). Además, EventSource permite para el consumo de los acontecimientos en el proceso, mediante la suscripción a ellos vía EventListener, también nuevo en el 4.5 de .NET Framework (más sobre esto momentáneamente).
El proveedor de eventos puede identificarse por nombre o GUID. Cada tipo de evento en particular a su vez se identifica por ID de evento y, opcionalmente, una palabra clave para distinguir de otros tipos sin relación de eventos disparados por este proveedor (TplEtwProvider no utiliza palabras clave). Hay parámetros opcionales de tarea y de operación que puede resultarle útil para filtrar, pero confío en ID de evento. Cada evento también define el nivel de detalle.
TPL eventos tienen una variedad de aplicaciones además de las cadenas de causalidad, como seguimiento de tareas durante el vuelo, telemetría y así sucesivamente. No dispararon para awaitables personalizado, sin embargo.
Introducción EventListener
En el .NET Framework 4, con el fin de capturar eventos ETW, tienes que ejecutar un oyente ETW out-of-process, como grabadora de rendimiento de Windows o de Vance Morrison PerfView y luego se correlacionan los datos capturados con el estado que observó en el depurador. Esto planteaba problemas adicionales, como se almacenan los datos fuera del espacio de memoria de proceso y volcados no incluyen, que hizo esta solución menos convenientes para la depuración de post-mortem. Por ejemplo, si confías en el informe de errores de Windows para proporcionar volcados, usted no tendrá ningún rastro ETW y así faltarán información de causalidad.
Sin embargo, a partir de los .NET Framework 4.5, es posible suscribirse a eventos TPL (y otros eventos disparados por herederos EventSource) vía System.Diagnostics.Tracing.EventListener (bit.ly/XJelwF). Esto permite la captura y conservación de segmentos de trazas de pila en el espacio de memoria del proceso. Por lo tanto, un minivolcado con montón debería bastar para extraer información de causalidad. En este artículo, voy a suscripciones de EventListener basadas sólo detalle.
Cabe destacar que la ventaja de un oyente de out-of-process es que usted puede conseguir siempre las pilas de llamadas escuchando a los eventos de ETW pila (confiar en una herramienta existente o haciendo tedioso pila caminando y dirección del módulo de seguimiento usted mismo). Al suscribirse a los eventos usando EventListener, te puedes información de la pila de llamada Windows Store aplicaciones, porque está prohibida la API StackTrace. (Más adelante se describe un enfoque que trabaja para aplicaciones Windows Store).
Para suscribirse a los eventos, se debe heredar de eventooyente, reemplace el método OnEventSourceCreated y asegúrese de que se crea una instancia de su oyente en cada dominio de aplicación de su programa (la suscripción es por dominio de aplicación). Después de crear la instancia de EventListener, llamará a este método para notificar al oyente de orígenes de eventos que se están creando. También proporcionará notificaciones de todas las fuentes de evento que existían antes de que el oyente se creó. Después de filtrar los orígenes de eventos por nombre o GUID (performance-wise, comparando el GUID es una mejor idea), una llamada a EnableEvents suscribe al oyente a la fuente:
private static readonly Guid tplGuid =
new Guid("2e5dba47-a3d2-4d16-8ee0-6671ffdcd7b5");
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Guid == tplGuid)
EnableEvents(eventSource, EventLevel.LogAlways);
}
Para procesar eventos, deberá implementar el método abstracto OnEventWritten. Con el fin de preservar y restaurar la pila traza segmentos, necesita captar la pila de llamadas justo antes de una operación asincrónica y entonces, cuando inicia su ejecución, asociado a un segmento de trazas de pila almacenado con él. Para correlacionar estos dos eventos, puede utilizar el parámetro TaskID. Parámetros pasados a un método de evento-cocción correspondiente en un origen de eventos se encajonó en una colección de sólo lectura del objeto y pasa como la propiedad de la carga útil de EventWrittenEventArgs.
Interesante, hay caminos rápidos especiales para eventos EventSource que se consumen como ETW (no a través de EventListener), donde el boxeo no se produce para sus argumentos. Esto proporciona una mejora de rendimiento, pero sobre todo es cero hacia fuera debido a la maquinaria de proceso cruzado.
En el método OnEventWritten, necesita distinguir entre fuentes de eventos (en caso que usted está suscrito a más de uno) e identificar el evento en sí. El seguimiento de la pila será capturado (almacenado) cuando fuego eventos TaskScheduled o TaskWaitBegin y asociado a una operación asincrónica recién iniciada (restaurada) en TaskWaitEnd. También debe pasar en taskId como el identificador de correlación. Figura 1 muestra el esquema de cómo se manejarán los acontecimientos.
Figura 1 gestión de los eventos TPL en el método de OnEventWritten
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (eventData.EventSource.Guid == tplGuid)
{
int taskId;
switch (eventData.EventId)
{
case 7: // Task scheduled
taskId = (int)eventData.Payload[2];
stackStorage.StoreStack(taskId);
break;
case 10: // Task wait begin
taskId = (int)eventData.Payload[2];
bool waitBehaviorIsSynchronous =
(int)eventData.Payload[3] == 1;
if (!waitBehaviorIsSynchronous)
stackStorage.StoreStack(taskId);
break;
case 11: // Task wait end
taskId = (int)eventData.Payload[2];
stackStorage.RestoreStack(taskId);
break;
}
}
}
Nota: Valores explícitos ("magic numbers") en el código son una mala práctica de programación y se utilizan aquí sólo por la brevedad. El proyecto de código de muestra que lo acompaña tiene convenientemente estructurado en constantes y enumeraciones para evitar duplicaciones y riesgo de errores tipográficos.
Tenga en cuenta que en TaskWaitBegin, busque TaskWaitBehavior ser sincrónico, que ocurre cuando una tarea se esperaba se ejecuta de forma sincrónica o ya ha terminado. En este caso, una pila de llamadas sincrónicas es todavía en su lugar, por lo que no necesita ser almacenada explícitamente.
Almacenamiento Local de Async
Cualquier estructura de datos que desea preservar la llamada pila segmentos necesita la calidad siguiente: Valor almacenado (cadena de causalidad) debe conservarse para cada operación asincrónica, el siguiente flujo de control en el camino a través de esperan límites y consecuencias, teniendo en cuenta que consecuencias pueden ejecutar en diferentes hilos.
Esto sugiere una variable local-filiformes que preserve su valor perteneciente a la actual operación asincrónica (una cadena de consecuencias), en lugar de un subproceso concreto. Se puede ser más o menos nombrado "async-local almacenamiento."
El CLR ya tiene una estructura de datos llamada ExecutionContext que ha capturado en un subproceso y restaurado por el otro (donde continuación llega a ejecutar), así se pasa junto con el flujo de control. Esto es esencialmente un contenedor que almacena otros contextos (SynchronizationContext, CallContext y así sucesivamente) que podrían ser necesarios para continuar la ejecución en exactamente el mismo entorno, donde fueron interrumpidos. Stephen Toub tiene los detalles en bit.ly/M0amHk. Lo más importante, puede almacenar datos arbitrarios en CallContext (por llamar a sus métodos estáticos LogicalSetData y LogicalGetData), que parece satisfacer el propósito ya mencionado.
Tenga en cuenta que CallContext (en realidad, internamente hay dos de ellos: LogicalCallContext y IllogicalCallContext) es un objeto pesado, diseñado para flujo a través de límites de comunicación remota. Cuando no hay datos personalizados se almacenan, el tiempo de ejecución no inicializa los contextos, ahorrando el costo de mantenerlos con el flujo de control. Tan pronto como se llama al método CallContext.LogicalSetData, un ExecutionContext mutable y varias tablas hash debe ser creado y han trascendido o clonado a partir de ese momento.
Por desgracia, ExecutionContext (junto con todos sus componentes) es capturado antes del incendio de eventos descrito TPL y restaurado poco después. Por lo tanto, cualquier datos personalizados guardados en CallContext en el medio se descartan una vez restaurado ExecutionContext, lo que resulta inadecuado para nuestro proposito.
Además, la clase de CallContext no está disponible en el subconjunto de aplicaciones .NET para Windows Store, así que una alternativa necesaria para este escenario.
Una forma de construir un almacenamiento local async que trabaja alrededor de estos problemas es mantener el valor en el almacenamiento local de subproceso (TLS) mientras se ejecuta la parte sincrónica del código. Entonces, cuando se desencadena el evento TaskWaitStart, almacenar el valor en un diccionario común (no-TLS), introducido por el TaskID. Cuando se desencadena el evento de la contraparte, TaskWaitEnd, quite el valor conservado del diccionario y guardarlo a TLS, posiblemente en un subproceso diferente.
Como saben, valores almacenados en TLS se conservan incluso después de que un subproceso se devuelve a los subprocesos y consigue nuevo trabajo a ejecutar. Así, en algún momento, el valor tiene que ser eliminado de TLS (de lo contrario, algunos otros operación asincrónica ejecutando en este hilo más tarde podría acceder el valor almacenado por la operación anterior, como si fuese su propia). Usted no puede hacerlo en el controlador de eventos TaskWaitBegin porque, en caso de anidado espera, TaskWaitBegin y TaskWaitEnd los acontecimientos ocurren varias veces, una vez por aguarda, y un valor almacenado puede ser necesaria en el medio, como en el siguiente fragmento:
async Task OuterAsync()
{
await InnerAsync();
}
async Task InnerAsync()
{
await Task.Delay(1000);
}
En cambio, es seguro que considerar que el valor de TLS es elegible para ser despejó cuando la operación asincrónica actual ya no está siendo ejecutada en un subproceso. El CLR tiene una en-evento de proceso que notificaría un hilo de rosca se reciclan hacia el grupo de subprocesos (hay un ETW uno —bit.ly/ZfAWrb), para ello utilizare ThreadPoolDequeueWork por FrameworkEventSource (también indocumentado), que ocurre cuando se inicia una nueva operación en un subproceso ThreadPool. Esto deja fuera a hilos no agrupados, para que usted tendría que limpiar manualmente los TLS, como cuando un subproceso de interfaz de usuario vuelve al bucle de mensajes.
Para una implementación funcional de este concepto junto con segmentos de pila captura y concatenación, consulte la clase StackStorage en la descarga de código fuente que lo acompaña. También hay una abstracción limpiador, AsyncLocal <T>, que permite almacenar cualquier valor y transferirlo con el flujo de control a posteriores continuaciones asincrónicas. Podrá utilizarlo como almacén de cadena de causalidad para escenarios de aplicaciones de Windows Store.
Seguimiento de causalidad en aplicaciones Windows Store
El enfoque descrito se sostuviera un aún en un escenario de Windows Store si se dispusiera de la API de System.Diagnostics.StackTrace. Para bien o para mal, no lo es, que significa que no puede obtener cualquier información sobre la llamada marcos de pila superior al actual de dentro del código. Así, aunque todavía se admiten eventos TPL, una llamada a TaskWaitStart o TaskWaitEnd está enterrada profundamente en las llamadas de método de marco, para que no tenga ninguna información sobre el código que causaron estos eventos al fuego.
Por suerte, .NET para Windows Store aplicaciones (como el .NET Framework 4.5) proporciona CallerMemberNameAttribute (bit.ly/PsDH0p) y sus compañeros CallerFilePathAttribute y CallerLineNumberAttribute. Cuando éstos presentan argumentos del método opcional, el compilador inicializará los argumentos con los valores correspondientes en tiempo de compilación. Por ejemplo, el código siguiente generará "Main() en c:\Full\Path\To\Program.cs en línea de 14":
static void Main(string[] args)
{
LogCurrentFrame();
}
static void LogCurrentFrame([CallerMemberName] string name = null,
[CallerFilePath] string path = null,
[CallerLineNumber] int line = 0)
{
Console.WriteLine("{0}() in {1} at line {2}", name, path, line);
}
Esto sólo permite el método de registro obtener información sobre el marco de la llamada, que significa que tendrá que garantizar que se llama de todos los métodos que desee capturados en la cadena de causalidad. Una ubicación conveniente para esto se adorna cada uno esperan expresión con una llamada a un método de extensión, como esto:
await WorkAsync().WithCausality();
Aquí, las capturas del método de WithCausality el marco actual, lo anexa a la cadena de causalidad y devuelve una tarea o awaitable (dependiendo de qué declaraciones de WorkAsync), que al terminar una quita el marco de la cadena de causalidad.
Como pueden ser esperadas varias cosas diferentes, debe haber varias sobrecargas del WithCausality. Esto es sencillo para una tarea <T> (y aún más fácil para una tarea):
public static Task<T> WithCausality<T>(this Task<T> task,
[CallerMemberName] string member = null,
[CallerFilePath] string file = null,
[CallerLineNumber] int line = 0)
{
var removeAction =
AddFrameAndCreateRemoveAction(member, file, line);
return task.ContinueWith(t => { removeAction(); return t.Result; });
}
Sin embargo, es más complicado para awaitables personalizados. Como saben, el compilador de C# permite esperar una instancia de cualquier tipo que sigue un patrón particular (ver bit.ly/AmAUIF), que hace escritura sobrecargas que acomodaría a cualquier encargo awaitable imposible usando estática escribir solamente. Usted puede hacer algunas sobrecargas de acceso directo para awaitables predefinidos en el marco, como YieldAwaitable o ConfiguredTaskAwaitable, o los definidos en la solución, pero en general hay que recurrir a la dinámica lengua Runtime (DLR). Todos los casos la manipulación requiere un montón de código repetitivo, así que siéntete libre estudiar el código fuente adjunto para detalles.
Vale la pena teniendo en cuenta que en caso de anidados espera, métodos WithCausality se ejecutará de interno a externo (como esperan se evalúan expresiones), por lo que debe tenerse cuidado para armar la pila en el orden correcto.
Cadenas de causalidad de visualización
Ambos enfoques descritos mantienen información de causalidad en la memoria como las listas de los segmentos de la pila de llamada o marcos. Sin embargo, les caminando y concatenar en una cadena de causalidad única para la exhibición es tedioso hacerlo a mano.
La opción más sencilla para automatizar esto es aprovechar el evaluador del depurador. En este caso, crear una propiedad pública estática (o método) en una clase pública, que, cuando llama, camina a la lista de segmentos almacenados y devuelve una cadena de causalidad concatenados. Entonces usted puede evaluar esta propiedad durante la depuración y ver el resultado en el visualizador de texto.
Por desgracia, este enfoque no funciona en dos situaciones. Uno se produce cuando el marco superior de la pila es en código nativo, que es un escenario común para depurar la aplicación cuelga, como primitivas de sincronización basado en kernel llamar a código nativo. El evaluador de depurador sólo mostraría, "No se puede evaluar expresión porque se optimiza el código del método actual" (Mike Stall describe estas limitaciones en detalle en bit.ly/SLlNuT).
La otra cuestión es con post-mortem de depuración. Realmente puede abrir un minivolcado en Visual Studio y sorprendentemente (dado que no existe ningún proceso para depurar, sólo su volcado de memoria), que le permite examinar los valores de las propiedades (ejecutar captadores de propiedad) e incluso llamar a algunos métodos! Esta increíble pieza de funcionalidad se construye en el depurador de Visual Studio y trabajos por interpretar la expresión de una reloj y todos los métodos que pone en (en contraste con la depuración en vivo, donde es ejecutado el código compilado).
Obviamente, existen limitaciones. Por ejemplo, mientras se hace la depuración de la descarga, usted no puede en cualquier llamada a métodos nativos (que significa que usted no puede incluso ejecutar un delegado, debido a su método Invoke se genera en código nativo) o acceso algunos restringida API (como System.Reflection). Evaluación basada en la intérprete también es lento esperar — y, lamentablemente, debido a un error, el tiempo de espera de evaluación para la depuración de la descarga se limita a 1 segundo en Visual Studio 2012, independientemente de la configuración. Esto, dado el número de llamadas a métodos necesarios para recorrer la lista de segmentos de trazas de pila e iterar sobre todos los marcos, prohíbe el uso del evaluador para este propósito.
Por suerte, el depurador siempre permite el acceso a los valores de campo (incluso en la depuración de la descarga y cuando el marco superior de la pila está en código nativo), que permite rastrear a través de los objetos que constituyen una cadena de causalidad almacenados y reconstruirlo. Esto es obviamente tedioso, así que escribí una extensión de Visual Studio que hace esto por usted (Ver código de muestra correspondiente). Figura 2 muestra lo que parece la experiencia final. Tenga en cuenta que el gráfico de la derecha también se genera por esta extensión y representa el equivalente de async de pilas paralelas.
Figura 2 cadena de causalidad para un método asincrónico y causalidad "Paralelo" para todos los subprocesos
Comparación y advertencias
Ambos métodos de seguimiento de causalidad no son libres. La segunda (llamada-información-base) es más ligero, ya que no implica la API StackTrace caro, confiando en su lugar en el compilador de información llamadas marco durante el tiempo de compilación, que significa "libre" en un programa en ejecución. Sin embargo, todavía utiliza infraestructura de eventos con su coste para apoyar AsyncLocal <T>. Por otra parte, el primer acercamiento proporciona más datos, no saltarse Marcos sin espera. También automáticamente realiza un seguimiento de varias otras situaciones donde se presenta la asincronía basada en tareas sin esperan, como el método de Task.Run; por otro lado, no funciona con awaitables personalizados.
Un beneficio adicional del rastreador TPL basadas en eventos es que vigente Código asincrónico no debe modificarse, mientras que para el enfoque de basadas en atributos de información de llamadas, usted tiene que alterar todos esperan la declaración en su programa. Pero sólo los último soporta Windows Store aplicaciones.
El rastreador de sucesos TPL padece también un montón de código de marco repetitivo en segmentos de la traza de pila, aunque puede ser fácilmente filtrada por el nombre de espacio de nombres o clase de marco. Ver el código de ejemplo para obtener una lista de filtros comunes.
Otra salvedad refiere lazos en código asíncrono. Considere el siguiente fragmento:
async static Task Loop()
{
for (int i = 0; i < 10; i++)
{
await FirstAsync();
await SecondAsync();
await ThirdAsync();
}
}
Al final del método, su cadena de causalidad crecería a más de 30 segmentos, varias veces alternando entre marcos FirstAsync, SecondAsync y ThirdAsync. Para un bucle finito, esto puede ser tolerable, aunque todavía es una pérdida de memoria para almacenar Marcos duplicados 10 veces. Sin embargo, en algunos casos, un programa podría introducir un bucle infinito válido, por ejemplo, en el caso de un bucle de mensajes. Por otra parte, repetición infinita podría introducirse sin lazo o esperan construcciones — un temporizador reprogramación a sí mismo en cada tick es un ejemplo perfecto. Seguimiento de una cadena de causalidad infinita es una forma segura de quedarse sin memoria, por lo tanto la cantidad de datos almacenados debe reducirse a una cantidad finita de alguna manera.
Este problema no afecta el rastreador de llamadas basadas en información, como quita un cuadro de la lista inmediatamente después del inicio de una continuación. Existen dos enfoques (combinables) para solucionar este problema para el escenario de eventos TPL. Uno es cortar los datos anteriores basados en la cantidad de almacenamiento máximo balanceo. La otra es representar bucles eficientemente y evitar la duplicación. Para ambos enfoques, también puede detectar patrones comunes de bucle infinito y cortar la cadena de causalidad explícitamente en estos puntos.
No dude en consultar el acompañamiento proyecto de muestra para ver cómo podría implementarse asa plegable.
Como se indicó, la API de eventos TPL sólo le permite capturar una cadena de causalidad, no un gráfico. Esto es porque los métodos Task.WaitAll y Task.WhenAll se implementan como cuenta atrás, donde la continuación está programado sólo cuando la última tarea viene en terminado y el contador llegue a cero. Así, sólo la última tarea completada forma una cadena de causalidad.
Conclusión
En este artículo, usted ha aprendido la diferencia entre una pila de llamadas, una pila de retorno y una cadena de causalidad. Ahora debe ser consciente de los puntos de extensión que .NET Framework proporciona para rastrear la programación y ejecución de operaciones asincrónicas y ser capaces de aprovechar estos para capturar y mantener las cadenas de causalidad. Los enfoques describen cubierta seguimiento de causalidad en clásico y aplicaciones de Windows Store, tanto en vivo y escenarios depuración post mortem. También aprendieron sobre el concepto de almacenamiento async-local y su posible aplicación para aplicaciones Windows Store.
Ahora puede seguir adelante y causalidad seguimiento en su modelo asincrónico de incorporar o utilizar almacenamiento async-local en cálculos paralelos; explorar los orígenes de eventos que ofrecen las aplicaciones .NET Framework 4.5 y .NET para Windows Store para construir algo nuevo, como un rastreador para tareas inconclusas en su programa; o utilizar este punto de extensión para disparar sus propios eventos para afinar el rendimiento de su aplicación.
Andriy (Andrew) Stasyuk es un Ingeniero de desarrollo de software a prueba II el equipo logró idiomas Microsoft. Cuenta con siete años de experiencia como participante, autor de la tarea, miembro del jurado y coach en varios concursos de programación nacionales e internacionales. Trabajó en el desarrollo de software financiero en Paladyne/Broadridge Financial Solutions Inc. y Deutsche Bank AG, antes de trasladarse a Microsoft. Sus principal interés en la programación son algoritmos, paralelismo y rompecabezas.
Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Vance Morrison y Lucian Wischik