Este artigo foi traduzido por máquina.

Programação assíncrona

Acompanhamento da corrente de causalidade assíncrona

Andrew Stasyuk

Baixar o código de exemplo

 

Com o advento do C# 5, Visual Basic .NET 11, 4.5 Microsoft .NET Framework e .NET para Windows Store apps, a experiência de programação assíncrona foi racionalizado grandemente. Novas assíncrona e esperam por palavras-chave (Async e Await em Visual Basic) permitem aos desenvolvedores manter a mesma abstração eles foram usados para quando escrever código síncrono.

Muito esforço foi colocado em Visual Studio 2012 para melhorar assíncrono de depuração com ferramentas como pilhas paralelas, tarefas paralelas, relógio paralela e o Visualizador de simultaneidade. No entanto, em termos de ser par com o código síncrono depuração experiência, nós não estamos lá ainda.

Uma das questões mais importantes que quebra a abstração e revela o encanamento interno por trás da fachada de async/esperam é a falta de informações de pilha de chamada no depurador. Neste artigo, vou fornecer meios para colmatar esta lacuna e melhorar a experiência de depuração assíncrona em seu aplicativo .NET 4.5 ou Windows Store.

Vamos resolver primeiro em terminologia essencial.

Definição de uma pilha de chamadas

Documentação do MSDN (bit.ly/Tukvkm) usado para definir o pilha de chamadas como "a série de chamadas de método levando desde o início do programa, a declaração que atualmente está sendo executado em tempo de execução." Esta noção foi perfeitamente válida para o modelo de programação síncrono, single-threaded, mas agora que o paralelismo e assincronismo estão ganhando impulso, taxonomia mais precisa é necessária.

Para efeitos deste artigo, é importante distinguir a cadeia de causalidade da pilha retorno. Dentro do paradigma síncrono, estes dois termos são em sua maioria idênticos (vou mencionar o caso excepcional mais tarde). No código assíncrono, a definição acima descreve uma cadeia de causalidade.

Por outro lado, a instrução que atualmente está sendo executada, quando terminar, vai levar a uma série de métodos, continuando a sua execução. Esta série constitui a pilha de retorno. Como alternativa, para os leitores familiarizados com a continuação passando estilo (Eric Lippert tem uma fabulosa série sobre este tema, começando com bit.ly/d9V0Dc), a pilha de retorno pode ser definida como uma série de continuações que são registrados para executar, deve concluir o método atualmente em execução.

Em poucas palavras, a cadeia de causalidade responde à pergunta, "Como eu consegui aqui?", enquanto a pilha de retorno é a resposta, "onde eu vou próximo?" Por exemplo, se você tem um bloqueio em seu aplicativo, você pode ser capaz de descobrir o que causou desde o primeiro, enquanto o último gostaria que você saiba quais são as consequências. Observe que enquanto uma cadeia de causalidade faixas sempre volta para o ponto de entrada do programa, a pilha de retorno é cortada no ponto onde o resultado da operação assíncrona não é observado (por exemplo, métodos void assíncronos ou trabalho agendado via ThreadPool. QueueUserWorkItem).

Há também uma noção de rastreamento de pilha, sendo uma cópia de uma pilha de chamada síncrona preservada para diagnósticos; Vou usar estes dois termos como sinônimos.

Esteja ciente de que existem várias suposições tácitas nas definições anteriores:

  • "Chamadas de método" referido na primeira definição geralmente implica "métodos que não completaram ainda," que tenha o significado físico de "estar na pilha" o modelo de programação síncrono. No entanto, enquanto nós geralmente não estamos interessados em métodos que já retornaram, não é sempre possível distingui-los durante a depuração assíncrono. Neste caso, não há nenhuma noção física de "estar na pilha" e todas as continuações são igualmente elementos válidos de uma cadeia de causalidade.
  • Mesmo em código síncrono, uma cadeia de causalidade e pilha retorno não são sempre idênticos. Um caso particular, em um método pode estar presente em um, mas a falta do outro, é uma chamada de cauda. Embora não diretamente expressáveis em c# e Visual Basic .NET, pode ser codificado em Intermediate Language (IL) (prefixo "cauda.") ou produzido pelo compilador just-in-time (JIT) (especialmente em um processo de 64 bits).
  • Por último, mas não menos importante, cadeias de causalidade e pilhas de retorno podem ser não-lineares. Ou seja, no caso mais geral, estão dirigidos gráficos tendo instrução atual como um coletor (gráfico de causalidade) ou fonte (gráfico de retorno). Não-linearidade no código assíncrono é devido à forks (operações assíncronas paralelas provenientes de um) e associações (continuação agendada para ser executado após a conclusão de um conjunto de operações assíncronas paralelas). Para efeitos deste artigo e devido às limitações de plataforma (explicadas mais tarde), vou considerar apenas cadeias de causalidade linear e retornar a pilhas, que são subconjuntos de gráficos correspondentes.

Felizmente, se assincronia é introduzido em um programa usando async e esperam por palavras-chave sem garfos ou junções, e todos os métodos assíncronos são aguardados, a cadeia de causalidade é ainda idêntica para a pilha de retorno, assim como no código síncrono. Neste caso, ambos são igualmente úteis para orientar-se no fluxo de controle.

Por outro lado, as cadeias de causalidade são raramente iguais ao retornar pilhas em programas empregando explicitamente programado continuações, um exemplo notável sendo o fluxo de dados Task Parallel Library (TPL). Isto é devido à natureza dos dados fluindo de um bloco de origem para um bloco de destino, nunca retornando ao primeiro.

Ferramentas existentes

Considere um exemplo rápido:

static void Main() {   OperationAsync().Wait(); } async static Task OperationAsync() {   await Task.Delay(1000);   Console.WriteLine("Where is my call stack?"); }

Extrapolando a abstração desenvolvedores foram usados para na depuração síncrono, eles seriam de esperar para ver a seguinte pilha de cadeia/retorno de causalidade quando a execução é interrompida no método Console. WriteLine:

ConsoleSample.exe!ConsoleSample.Program.OperationAsync() Line 19 ConsoleSample.exe!ConsoleSample.Program.Main() Line 13

Mas se você tentar fazer isso, você verá que o método Main está ausente, enquanto o rastreamento de pilha começa diretamente no método OperationAsync precedido por [retomar Async método] na janela pilha de chamadas. Pilhas paralelas tem dois métodos; no entanto, ele não mostra que o principal chama OperationAsync. Tarefas paralelas não ajuda, mostrando "Sem tarefas para exibir."

Observação: Neste ponto o depurador está ciente do método Main, sendo parte da pilha de chamadas — você deve ter notado que pelo plano de fundo cinzento para trás a chamada para OperationAsync. O CLR e o Runtime do Windows (WinRT) tem que saber onde continuar a execução, após o quadro de pilha superior retorna; Assim, eles realmente armazenar pilhas de retorno. Neste artigo, porém, eu vou apenas mergulhar causalidade monitoramento, deixando pilhas de retorno como um tópico para outro artigo.

Preservando a cadeias de causalidade

De fato, cadeias de causalidade nunca são armazenadas pelo tempo de execução. Mesmo as pilhas de chamadas que você vê quando estiver depurando código síncrono são, em essência, retornam pilhas — como foi apenas disse, eles são necessários para o CLR e o tempo de execução do Windows saber quais os métodos para executar após o quadro superior retorna. O tempo de execução não precisa saber o que causou um método específico executar.

Para ser capaz de ver cadeias de causalidade durante ao vivo e post-mortem de depuração, você tem que explicitamente preservá-los ao longo do caminho. Presumivelmente, isso exigiria que armazenar informações de rastreamento de pilha (síncrono) em cada ponto onde está prevista a continuação e restaurar esses dados quando continuação começa a executar. Estas stack trace segmentos, em seguida, podem ser costurados juntos para formar uma cadeia de causalidade.

Estamos mais interessados em transferir informações de causalidade todo esperam por construções, como este é onde quebra a abstração de semelhança com código síncrono. Vamos ver como e quando esses dados podem ser capturados.

Como assinala Stephen Toub (bit.ly/yF8eGu), desde que FooAsync retorna uma tarefa, o código a seguir:

await FooAsync(); RestOfMethod();

é transformado pelo compilador para um equivalente áspero do presente:

var t = FooAsync(); var currentContext = SynchronizationContext.Current; t.ContinueWith(delegate {   if (currentContext == null)     RestOfMethod();   else     currentContext.Post(delegate { RestOfMethod(); }, null); }, TaskScheduler.Current);

Olhando o código expandido, parece que há pelo menos dois pontos de extensão que pode permitir a captura de informações de causalidade: TaskScheduler e SynchronizationContext. Na verdade, ambos oferecem pares semelhantes de métodos virtuais onde é possível capturar os segmentos de pilha de chamada nos momentos certas: QueueTask/TryDequeue TaskScheduler e Post/OperationStarted sobre SynchronizationContext.

Infelizmente, você só pode substituir padrão TaskScheduler quando explicitamente Agendando um representante por meio da API de TPL, tais como Task.Run, ContinueWith, StartNew e assim por diante. Isto significa que sempre que a continuação é agendada fora uma tarefa em execução, o padrão TaskScheduler estará em vigor. Assim, o TaskScheduler -­abordagem baseada em não será capaz de capturar as informações necessárias.

Quanto a SynchronizationContext, embora seja possível substituir a instância padrão dessa classe para o segmento atual chamando o método SynchronizationContext.SetSynchronizationContext, isso tem que ser feito para todos os threads do aplicativo. Assim, você teria que ser capaz de controle segmento tempo de vida, que é inviável, se você não está planejando para reimplementar a um pool de segmentos. Além disso, Windows Forms, Windows Presentation Foundation (WPF) e ASP.NET fornecem suas próprias implementações de SynchronizationContext além de SynchronizationContext.Default, que a agenda de trabalho para o pool de segmentos. Portanto, sua implementação teria um comportamento diferente dependendo da origem do segmento em que está trabalhando.

Observe também que quando aguardando um costume awaitable, é inteiramente até implementação se deve usar o SynchronizationContext agendar uma continuação.

Felizmente, existem dois pontos de extensão apropriado para o nosso cenário: subscrever eventos TPL sem ter que modificar a base de código existente, ou explicitamente optar em modificando ligeiramente todos esperam por expressão no aplicativo. A primeira abordagem funciona apenas em aplicações desktop do .NET, enquanto a segunda pode acomodar apps da loja do Windows. Eu vou detalhar tanto nas seções a seguir.

Introdução EventSource

Evento de rastreamento para Windows (ETW), tendo definido o evento provedores para praticamente todos os aspectos do tempo de execução oferece suporte a .NET Framework (bit.ly/VDfrtP). Particularmente, TPL aciona eventos que permitem que você acompanhe a vida de tarefa. Embora nem todos esses eventos estão documentados, você pode obter suas definições-se aprofundar em mscorlib. dll com uma ferramenta como ILSpy ou refletor ou inspecionar em fonte de referência de quadro (bit.ly/HRU3) e pesquisando para a classe de TplEtwProvider. Naturalmente, aplica-se a disclaimer usual de reflexão: Se a API não é documentada, não há nenhuma garantia de que o comportamento observado empiricamente será mantido na próxima versão.

TplEtwProvider herda de System.Diagnostics.Tracing.EventSource, que foi introduzido no .NET Framework 4.5 e agora é uma maneira recomendada para acionar eventos ETW em seu aplicativo (anteriormente era necessário lidar com manual ETW manifesto geração). Além disso, EventSource permite consumo de eventos no processo, assinando-os via EventListener, também nova no .NET Framework 4.5 (mais momentaneamente).

O provedor de eventos pode ser identificada por um nome ou GUID. Cada tipo de evento particular por sua vez é identificado pelo ID de evento e, opcionalmente, uma palavra-chave para distinguir de outros tipos independentes de eventos acionados por esse provedor (TplEtwProvider não usa palavras-chave). Existem parâmetros opcionais de tarefa e CONV você pode achar útil para filtragem, mas eu vou depender exclusivamente ID de evento. Cada evento também define o nível de verbosidade.

TPL eventos têm uma variedade de usos, além de cadeias de causalidade, como o acompanhamento das tarefas durante o voo, telemetria e assim por diante. Eles não fogo para awaitables personalizada, porém.

Introdução EventListener

No .NET Framework 4, a fim de capturar eventos ETW, você tinha que estar executando um ouvinte de ETW de out-of-process, tais como gravador de desempenho do Windows ou em Vance Morrison PerfView e então correlacionar dados capturados com o estado que você observou no depurador. Isto causou problemas adicionais, como os dados foram armazenados fora do espaço de memória do processo e despejos não incluem-lo, que fez com que essa solução menos adequados para depuração de post-mortem. Por exemplo, se você confiar em relatório de erros do Windows para fornecer lixeiras, você não vai obter qualquer rastreamentos ETW e assim informações de causalidade será ausentes.

No entanto, a partir do .NET Framework 4.5, é possível inscrever-se para eventos TPL (e outros eventos acionados por herdeiros EventSource) via System.Diagnostics.Tracing.EventListener (bit.ly/XJelwF). Isto permite a captura e a preservação de segmentos de rastreamento de pilha no espaço de memória do processo. Portanto, um mini-despejo de heap deve ser suficiente para extrair informações de causalidade. Neste artigo, vou apenas detalhes baseados em EventListener assinaturas.

Vale ressaltar que a vantagem de um ouvinte de fora do processo é que você pode sempre começar as pilhas de chamadas por ouvir os eventos ETW de pilha (baseando-se em uma ferramenta existente ou fazendo tediosa pilha curta e endereço do módulo de rastreamento-se). Ao inscrever-se para os eventos usando EventListener, você não pode obter informações de pilha de chamada no Windows Store apps, porque a API StackTrace é proibida. (Uma abordagem que funciona para Windows Store apps é descrita mais tarde).

Para se inscrever em eventos, você deve herdar de evento­ouvinte, substituir o método OnEventSourceCreated e certifique-se de que uma instância do seu ouvinte criada em cada AppDomain do seu programa (a assinatura é por domínio de aplicativo). Depois EventListener é instanciada, esse método será chamado para notificar o ouvinte de fontes de eventos que estão sendo criados. Ele também irá fornecer notificações de todas as fontes de evento que existia antes do ouvinte foi criado. Após a filtragem de fontes de eventos por nome ou GUID (performance-wise, comparar os GUIDs é uma idéia melhor), uma chamada para EnableEvents assina o ouvinte à fonte:

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 processar eventos, você precisa implementar o método abstrato OnEventWritten. Com a finalidade de preservar e restaurar os segmentos de rastreamento de pilha, você precisa capturar a pilha de chamadas direito antes de uma operação assíncrona está programada e, em seguida, quando inicia a execução, associar um segmento de rastreamento de pilha armazenado. Para correlacionar estes dois eventos, você pode usar o parâmetro TaskID. Parâmetros passados para um método de acionamento de evento correspondente em uma fonte de evento são encaixotados em uma coleção de somente leitura do objeto e passados como a propriedade de carga de EventWrittenEventArgs.

Curiosamente, há caminhos rápidos especiais para eventos EventSource consumidos como ETW (não via EventListener), onde o boxe não ocorre em seus argumentos. Isso fornece uma melhoria de desempenho, mas é principalmente zerada devido à maquinaria de processo cruzado.

No método OnEventWritten, você precisa distinguir entre fontes de evento (no caso de que você se inscrever para mais de um) e identificar o evento em si. O rastreamento de pilha será capturado (armazenadas) em eventos de TaskScheduled ou TaskWaitBegin de fogo e associado com uma recém-iniciado operação assíncrona (restaurada) em TaskWaitEnd. Você também precisará passar taskId como o identificador de correlação. Figura 1 mostra o esboço de como os eventos serão manipulados.

Figura 1 manipulação de eventos TPL no método 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;     }   } }

Observação: Valores explícitos ("magic numbers") no código são uma má prática de programação e são usados aqui apenas para fins de brevidade. O projeto de código de exemplo que acompanha tem-los convenientemente estruturado em constantes e enumerações para evitar duplicações e risco de erros de digitação.

Note-se que em TaskWaitBegin, check para TaskWaitBehavior ser síncrono, o que acontece quando uma tarefa está sendo aguardada é executada de forma síncrona, ou já foi concluída. Neste caso, uma pilha de chamada síncrona é ainda no lugar, então ele não precisa ser armazenado explicitamente.

Async-Local armazenamento

Qualquer estrutura de dados que você escolher para preservar os segmentos de pilha de chamada precisa da qualidade a seguir: O valor armazenado (Cadeia de causalidade) deve ser preservado para cada operação assíncrona, o seguinte fluxo de controle ao longo do caminho entre esperam por limites e continuação, tendo em conta que a continuação pode ser executado em threads diferentes.

Isto sugere uma thread-local-como variável que preserve seu valor referente para a atual operação assíncrona (uma cadeia de continuação), em vez de um determinado segmento. Ele pode ser aproximadamente denominado "async-local armazenamento."

O CLR já tem uma estrutura de dados chamada ExecutionContext que foi capturado em um segmento e restaurado por outro (onde a continuação começa a executar), assim sendo passado junto com o fluxo de controle. Este é essencialmente um recipiente que armazena outros contextos (SynchronizationContext, CallContext e assim por diante) que podem ser necessários para continuar a execução em exatamente o mesmo ambiente, onde eles foram interrompidos. Stephen Toub tem os detalhes em bit.ly/M0amHk. Mais importante ainda, você pode armazenar dados arbitrários em CallContext (chamando seus métodos estáticos, LogicalSetData e LogicalGetData), que parece atender a finalidade acima mencionada.

Tenha em mente que CallContext (na verdade, internamente existem dois deles: LogicalCallContext e IllogicalCallContext) é um objeto pesado, projetado para fluir através de limites de comunicação remota. Quando nenhum dados personalizados são armazenados, o runtime não inicializar os contextos, poupando o custo de mantê-los com o fluxo de controle. Assim que você chamar o método CallContext.LogicalSetData, um ExecutionContext mutável e várias tabelas de hash têm de ser criados e repassados ou clonado daí em seguida diante.

Infelizmente, ExecutionContext (juntamente com todos os seus componentes) é capturado antes do incêndio de eventos descrito TPL e restaurado logo em seguida. Assim, quaisquer dados personalizados salvos em CallContext entre serão descartados após o ExecutionContext é restaurado, o que os torne impróprios para o nosso propósito específico.

Além disso, a classe CallContext não está disponível o subconjunto de aplicativos .NET para Windows Store, portanto, uma alternativa é necessária para esse cenário.

Uma maneira de construir um armazenamento local de async que iria contornar esses problemas é manter o valor no armazenamento local de thread (TLS), enquanto a parte síncrona do código está em execução. Então, quando o evento TaskWaitStart, armazene o valor em um dicionário comum (não-TLS), alinhado pelo TaskID. Quando o evento da contraparte, TaskWaitEnd, remova o valor preservado do dicionário e salvá-lo para trás para TLS, possivelmente em um thread diferente.

Como você deve saber, os valores armazenados no TLS são preservados mesmo depois que um segmento é retornado para o pool de segmentos e obtém novo trabalho para executar. Assim, em algum momento, o valor deve ser retirado de TLS (caso contrário, alguns outros operação assíncrona executar sobre este tópico mais tarde pode acessar o valor armazenado pela operação anterior, como se fosse seu próprio). Você não pode fazer isso no manipulador de eventos TaskWaitBegin, porque, no caso de aninhados aguarda, TaskWaitBegin e TaskWaitEnd eventos ocorrem várias vezes, uma vez por await, e um valor armazenado pode ser necessária no meio, tal como o trecho a seguir:

async Task OuterAsync() {   await InnerAsync(); } async Task InnerAsync() {   await Task.Delay(1000); }

Em vez disso, é seguro considerar que o valor no TLS é elegível para ser limpo quando a operação assíncrona atual não está sendo executada em um segmento. Porque o CLR não tem um em-­evento de processo que iria notificar de um thread que está sendo reciclado volta para o pool de threads (há um ETW em um —bit.ly/ZfAWrb), para o efeito vou usar ThreadPoolDequeueWork acionado por FrameworkEventSource (também não documentado), que ocorre quando uma nova operação é iniciada em um thread do pool. Isso deixa de fora os segmentos não-agrupado, para a qual você teria que limpar manualmente o TLS, como quando um thread da interface do usuário retorna para o loop de mensagem.

Para uma implementação de trabalho deste conceito juntamente com segmentos de pilha capturando e concatenação, consulte a classe StackStorage no download do código fonte fornecido. Há também uma abstração mais limpa, AsyncLocal <T>, que permite armazenar qualquer valor e transferi-lo com o fluxo de controle para posterior continuação assíncrona. Vou usá-lo como armazenamento de cadeia de causalidade para cenários de aplicativos Windows Store.

Rastreamento de causalidade em Apps da loja do Windows

A abordagem descrita seria ainda realizar-se em um cenário de armazenamento do Windows se dispunha-se a API System.Diagnostics.StackTrace. Para melhor ou para pior, ele não é, que significa que você não pode obter qualquer informação sobre chamada de quadros de pilha acima do atual de seu código. Assim, mesmo quando ainda há suporte para eventos TPL, uma chamada para TaskWaitStart ou TaskWaitEnd é enterrada em chamadas de método da quadro, para que você não tenha nenhuma informação sobre o seu código que causou esses eventos para o fogo.

Felizmente, o .NET para Windows Store apps (como o .NET Framework 4.5) fornece CallerMemberNameAttribute (bit.ly/PsDH0p) e seus pares, CallerFilePathAttribute e CallerLine­NumberAttribute. Quando argumentos de método opcional estão decorados com estes, o compilador inicializará os argumentos com valores correspondentes em tempo de compilação. Por exemplo, o código a seguir produzirá "Main () em c:\Full\Path\To\Program.cs na linha 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); }

Isso permite que apenas o método de registro obter informações sobre o quadro de chamada, o que significa que você tem que garantir que é chamado de todos os métodos que você quer capturados na cadeia de causalidade. Uma localização conveniente para isso iria ser decorar cada aguardem expressão com uma chamada para um método de extensão, como este:

await WorkAsync().WithCausality();

Aqui, o WithCausality método captura o quadro atual, acrescenta à cadeia de causalidade e retorna a uma tarefa ou awaitable (dependendo de qual retorna WorkAsync), que após a conclusão do original remove o quadro da cadeia de causalidade.

Como podem ser aguardadas várias coisas diferentes, deve haver várias sobrecargas de WithCausality. Isso é simples para uma tarefa <T> (e ainda mais fácil para uma tarefa...):

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; }); }

No entanto, é mais complicado para awaitables personalizados. Como você deve saber, o compilador c# permite que você a esperar por uma instância de qualquer tipo que segue um determinado padrão (consulte bit.ly/AmAUIF), que faz exclusivamente sobrecargas que iriam acomodar qualquer personalizado awaitable impossível usando o estático digitando apenas. Você pode fazer alguns sobrecargas do atalho para awaitables predefinidos no quadro, como YieldAwaitable ou ConfiguredTaskAwaitable — ou aquelas definidas em sua solução, mas em geral você tem que recorrer para Runtime de linguagem dinâmico (DLR). Manipulação de todos os casos requer um monte de código clichê, então sinta-se livre para olhar para o código-fonte que acompanha para obter detalhes.

Também vale notar que, no caso de aninhados aguarda, WithCausality métodos serão executados do interior para o exterior (como esperam por expressões são avaliadas), então o cuidado deve ser tomado para montar a pilha na ordem correta.

Cadeias de causalidade de visualização

Ambas as abordagens descritas manter informações de causalidade na memória, como listas de segmentos de pilha de chamada ou quadros. No entanto, caminhando-los e concatenação em uma cadeia de causalidade única para exibição é entediante fazer à mão.

A opção mais fácil de automatizar isso é aproveitar o avaliador de depurador. Nesse caso, você criar uma propriedade público estática (ou método) em uma classe pública, que, quando chamado, anda a lista de segmentos armazenados e retorna uma cadeia de causalidade concatenados. Em seguida, você pode avaliar esta propriedade durante a depuração e ver o resultado no Visualizador de texto.

Infelizmente, essa abordagem não funciona em duas situações. Um ocorre quando o quadro de pilha de nível superior é em código nativo, que é um cenário comum para depurar travamentos de aplicativos, como os primitivos de sincronização baseado em kernel chamar código nativo. O avaliador de depurador exibiria apenas, "Não é possível avaliar expressão, porque o código do método atual é otimizado" (Mike Stall descreve essas limitações detalhadamente em bit.ly/SLlNuT).

A outra questão é com depuração post-mortem. Você pode realmente abrir um mini-despejo no Visual Studio e, surpreendentemente (dado que não há nenhum processo depurar, apenas seu despejo de memória), você está autorizado a examinar os valores de propriedade (Propriedade getters de correr) e até mesmo chamar alguns métodos! Essa incrível peça de funcionalidade é incorporada a Visual Studio depurador e obras por interpretar uma expressão de relógio e todos os métodos que ele chama (em contraste com a depuração ao vivo, onde o código compilado é executado).

Obviamente, há limitações. Por exemplo, ao fazer a depuração de despejo, você não pode em qualquer chamar de forma métodos nativos (o que significa que você ainda não é possível executar um delegado, porque seu método Invoke é gerado em código nativo) ou acesso alguns restritos APIs (como System. Reflection). Avaliação baseada em intérprete também é lenta conforme esperado — e, infelizmente, devido a um bug, o tempo limite de avaliação para depuração de despejo é limitado a 1 segundo em Visual Studio 2012, independentemente da configuração. Isso, dado o número de chamadas de método necessário para percorrer a lista de segmentos de rastreamento de pilha e iterar em todos os quadros, proíbe o uso do avaliador para esta finalidade.

Felizmente, o depurador sempre permite o acesso aos valores de campo (mesmo em depuração de despejo ou quando o quadro de pilha superior é em código nativo), que torna possível rastrear através dos objetos que constituem uma cadeia de causalidade armazenado e reconstruí-la. Esta é, obviamente, tediosa, então eu escrevi uma extensão Visual Studio que faz isso por você (Veja o que acompanha o código de exemplo). Figura 2 mostra o que parece a experiência final. Observe que o gráfico à direita também é gerado por esta extensão e representa o equivalente a async pilhas paralelas.


Figura 2 cadeia de causalidade para um método assíncrono e causalidade "Paralela" para todos os segmentos

Comparação e advertências

Duas abordagens de acompanhamento de causalidade não são livres. O segundo (chamador informação baseado) é mais leve, como não envolve o caro API StackTrace, baseando-se no compilador para fornecer o chamador informações do quadro durante o tempo de compilação, que significa "livre" em um programa em execução. No entanto, ele ainda usa infra-estrutura de eventos com seu custo para apoiar a AsyncLocal <T>. Por outro lado, a primeira abordagem fornece mais dados, não saltar frames sem espera. Ele automaticamente controla várias outras situações onde o assincronismo baseada em tarefa surge sem aguardam, como o método de Task.Run; por outro lado, ele não funciona com awaitables personalizado.

Um benefício adicional do controlador TPL baseada em eventos é que o código assíncrono existente não tem que ser modificado, enquanto que para a abordagem baseada em atributos chamador informação, você tem que alterar cada aguardam declaração em seu programa. Mas só os apps da loja do Windows suporta este último.

O controlador de eventos TPL também sofre de um monte de código de estrutura de clichê em segmentos de rastreamento de pilha, embora ele pode ser facilmente filtrado pelo nome do namespace ou classe frame. Consulte o código de exemplo para obter uma lista de filtros comuns.

Outra ressalva diz respeito aos loops no código assíncrono. Considere o seguinte trecho:

 

async static Task Loop() {   for (int i = 0; i < 10; i++)   {     await FirstAsync();     await SecondAsync();     await ThirdAsync();   } }

 

No final do método, sua cadeia de causalidade iria crescer para mais de 30 segmentos, repetidamente alternando entre quadros FirstAsync, SecondAsync e ThirdAsync. Para um loop finito, isso pode ser tolerável, embora ainda seja um desperdício de memória para armazenar quadros duplicados 10 vezes. No entanto, em alguns casos, um programa pode introduzir um loop infinito válido, por exemplo, no caso de um loop de mensagem. Além disso, a repetição infinita pode ser introduzida sem laço ou aguardam construções — um timer reescalonamento própria em cada tique é um exemplo perfeito. Uma cadeia de causalidade infinita de rastreamento é uma maneira de ficar sem memória, então a quantidade de dados armazenados tem de ser reduzido a uma quantidade finita de alguma forma.

Esse problema não afeta o tracker baseada em informação de chamador, como ele remove um quadro da lista imediatamente após o início da prorrogação. Existem duas abordagens (combinadas) para corrigir isso para o cenário de eventos TPL. Um é para cortar os dados mais antigos, baseados na quantidade máxima de armazenamento rolamento. O outro é para representar loops com eficiência e evitar duplicações. Para ambas as abordagens, você também pode detectar padrões comuns de loop infinito e cortar a cadeia de causalidade explicitamente esses pontos.

Não hesite em consultar o projeto de exemplo que acompanha para ver como o dobramento de loop pode ser implementada.

Como afirmado, a API de eventos TPL só lhe permite capturar uma cadeia de causalidade, não um gráfico. Isso ocorre porque o WaitAll e Task.WhenAll métodos é implementado como contagens regressivas, onde a continuação é agendada somente quando a última tarefa vem em concluído e o contador chega a zero. Assim, somente a última tarefa concluída forma uma cadeia de causalidade.

Conclusão

Neste artigo, você aprendeu a diferença entre uma pilha de chamadas, uma pilha de retorno e uma cadeia de causalidade. Agora você deve estar ciente dos pontos de extensão que o .NET Framework fornece para controlar o agendamento e execução de operações assíncronas e ser capaz de aproveitar estas para capturar e preservar cadeias de causalidade. As abordagens descritas tampa rastreamento causalidade no classic e Windows Store apps, ambos em ao vivo e cenários de depuração post-mortem. Você também aprendeu sobre o conceito de armazenamento local de async e sua possível aplicação para Windows Store apps.

Agora você pode ir adiante e incorporar o rastreamento em sua base de código assíncrono de causalidade ou usar async-local armazenamento em paralelos cálculos; explorar as fontes de evento que os aplicativos .NET Framework 4.5 e .NET para Windows Store oferecem para construir algo novo, como um tracker para tarefas não concluídas no seu programa; ou usar este ponto de extensão para disparar seus próprios eventos para aperfeiçoar o desempenho do seu aplicativo.

Andriy (Andrew) Stasyuk é engenheiro de desenvolvimento de software de teste II a equipe gerenciado linguagens Microsoft. Ele tem sete anos de experiência como participante, autor da tarefa, membro do júri e treinador em vários concursos de programação nacionais e internacionais. Trabalhou em desenvolvimento de software financeiro na Paladyne/Broadridge Financial Solutions Inc. e o Deutsche Bank AG, antes de se mudar para a Microsoft. Seus principais interesses na programação são algoritmos, paralelismo e quebra-cabeças.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Vance Morrison e Lucian Wischik