Il presente articolo è stato tradotto automaticamente.

Programmazione asincrona

Monitoraggio asincrono della catena di causalità

Andrew Stasyuk

Scarica il codice di esempio

 

Con l'avvento di C# 5, Visual Basic .NET 11, il Microsoft .NET Framework 4.5 e .NET per Windows Store apps, l'esperienza di programmazione asincrona è stato snellito notevolmente.Nuovo async e attendono Parole chiavi (Async e attendono in Visual Basic) consentono agli sviluppatori di mantenere la stessa astrazione erano usati per la scrittura di codice sincrono.

Un grande sforzo è stato messo in Visual Studio 2012 per migliorare asincrono debug con strumenti quali stack in parallelo, attività in parallelo, parallelo Guarda e concorrenze.Tuttavia, in termini di essere alla pari con il codice sincrono esperienza di debug, non siamo abbastanza là ancora.

Uno dei problemi più importanti che rompe l'astrazione e rivela impianto idraulico interno dietro la facciata async/attendono è la mancanza di informazioni sullo stack di chiamata nel debugger.In questo articolo, ho intenzione di fornire mezzi per colmare questa lacuna e migliorare l'esperienza di debug asincrona nella tua applicazione .NET 4.5 o Windows Store.

Let's depositarsi sulla terminologia essenziale in primo luogo.

Definizione di uno Stack di chiamate

Documentazione MSDN (bit.ly/Tukvkm) usato per definire nello stack di chiamate come "la serie di chiamate al metodo che conduce fin dall'inizio del programma per l'istruzione attualmente in esecuzione in fase di esecuzione". Questa nozione era perfettamente valida per il modello di programmazione a thread singolo, sincrono, ma ora che il parallelismo e asincronia stanno guadagnando slancio, tassonomia più precisa è necessaria.

Ai fini di questo articolo, è importante distinguere la catena di causalità dallo stack di ritorno.All'interno del paradigma sincrono, questi due termini sono per lo più identici (potrai citare il caso eccezionale successivamente).Nel codice asincrono, la suddetta definizione descrive una catena di causalità.

D'altra parte, l'istruzione attualmente in esecuzione, quando finito, porterà ad una serie di metodi continuando la loro esecuzione.Questa serie costituisce lo stack di ritorno.In alternativa, per i lettori di familiarità con la continuazione passando stile (Eric Lippert ha una serie favolosa su questo argomento, a partire da bit.ly/d9V0Dc), lo stack di ritorno potrebbe essere definito come una serie di continuazioni che sono registrati per eseguire, dovrebbe completare il metodo attualmente in esecuzione.

In poche parole, la catena di causalità risponde alla domanda, "Come ho qui?", mentre il ritorno dello stack è la risposta, "dove vado prossimo?" Ad esempio, se hai un deadlock dell'applicazione, potrebbe essere in grado di scoprire che cosa causato dal precedente, mentre quest'ultimo vorrei sapere quali sono le conseguenze.Si noti che, mentre una catena di causalità canzoni sempre verso il punto di ingresso del programma, il ritorno dello stack è tagliato fuori nel punto in cui il risultato dell'operazione asincrona non è osservato (per esempio, metodi void async o lavoro programmato via ThreadPool. QueueUserWorkItem).

C'è anche una nozione di traccia dello stack, essendo una copia di uno stack di chiamata sincrona conservato per la diagnostica; Io uso questi due termini in modo intercambiabile.

Essere consapevoli che ci sono parecchie assunzioni inespresse nelle definizioni precedenti:

  • "Chiamate al metodo" indicato nella definizione del prima generalmente implica "metodi che non hanno ancora completato" che portano il significato fisico di "essere sullo stack" nel modello di programmazione sincrono.Tuttavia, mentre noi non siamo generalmente interessati a metodi che hanno già restituito, esso non è sempre possibile distinguerli durante il debug asincrona.In questo caso, non non c'è nessuna nozione fisica di «essere sullo stack» e tutte le continuazioni sono ugualmente validi elementi di una catena di causalità.
  • Anche nel codice sincrona, una catena di causalità e il ritorno dello stack non sempre identici.Un caso particolare, quando un metodo potrebbe essere presente in uno, ma manca l'altro, è una chiamata coda.Anche se non direttamente esprimibili in C# e Visual Basic .NET, può essere codificato in Intermediate Language (IL) ("coda." prefisso) o prodotto dal compilatore just-in-time (JIT) (soprattutto in un processo a 64 bit).
  • Ultimo, ma non meno importante, catene di causalità e restituire pile possono essere non lineare.Ovvero, nel caso più generale, essi stanno diretto grafici avendo istruzione corrente come un lavandino (grafico causalità) o origine (ritorno grafico).Non linearità nel codice asincrono è dovuto alle forcelle (parallele operazioni asincrone provenienti da uno) e join (continuazione pianificati per essere eseguiti al completamento di un insieme di operazioni asincrone in parallelo).Ai fini del presente articolo e a causa delle limitazioni di piattaforma (spiegati più avanti), ti considero solo catene di causalità lineare e restituire pile, che sono sottoinsiemi dei grafici corrispondenti.

Per fortuna, se asincronia è introdotto in un programma utilizzando async attendono Parole chiavi senza forche o join e tutti i metodi asincroni sono atteso, la catena di causalità è ancora identica allo stack di ritorno, proprio come nel codice sincrono.In questo caso, entrambi sono ugualmente utili per orientarsi nel flusso di controllo.

D'altra parte, catene di causalità sono raramente uguale a restituire pile in programmi che impiegano esplicitamente prevista continuazioni, un esempio notevole è dataflow Task Parallel Library (TPL).Questo è dovuto alla natura dei dati che scorre da un blocco di origine a un blocco di destinazione, mai ritornare l'ex.

Strumenti esistenti

Si consideri un esempio rapido:

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

Estrapolando l'astrazione gli sviluppatori sono stati utilizzati per debug sincrona, si aspetta di vedere il seguente stack di ritorno catena di causalità quando l'esecuzione è in pausa presso il metodo Console. WriteLine:

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

Ma se si prova questo, troverete che nella finestra Stack di chiamate il metodo principale è manca, mentre la traccia dello stack inizia direttamente nel metodo OperationAsync preceduto da [Metodo Async ripresa]. Stack in parallelo ha entrambi i metodi; Tuttavia, non mostra che la principale chiama OperationAsync. Attività parallele non aiuta neanche, non mostrando "Nessuna attività per visualizzare".

Nota: A questo punto il debugger è consapevole del metodo Main facenti parte dello stack di chiamata — potreste aver notato che dallo sfondo grigio dietro la chiamata a OperationAsync. Il CLR e Windows Runtime (WinRT) devono sapere dove continua l'esecuzione dopo il frame dello stack topmost restituisce; così, infatti immagazzinano gli stack di ritorno. In questo articolo, però, io sarò solo approfondire causalità tracking, lasciando gli stack di ritorno come un argomento per un altro articolo.

Conservare le catene di causalità

In realtà, catene di causalità non sono mai memorizzate dal runtime. Anche gli stack di chiamate che si vede durante il debug di codice sincroni sono, in sostanza, restituiscono pile — come è stato appena detto, sono necessari per il CLR e Windows Runtime sapere quali metodi di eseguire dopo restituisce il telaio più in alto. Il runtime non ha bisogno di sapere che cosa ha causato un particolare metodo di esecuzione.

Per essere in grado di vedere catene di causalità durante il live e post-mortem di debug, è necessario preservarli esplicitamente lungo la strada. Presumibilmente, questo richiederebbe l'archiviazione delle informazioni di traccia dello stack (sincrono) in ogni punto dove è prevista la continuazione e ripristino dati quando continuazione inizia a eseguire. Questi stack trace segmenti potrebbero quindi essere cuciti insieme per formare una catena di causalità.

Siamo più interessati a trasferire informazioni di causalità attendono attraverso costrutti, come questo è dove si rompe astrazione di somiglianza con codice sincrono. Vediamo come e quando questi dati possono essere catturati.

Come sottolinea Stephen Toub (bit.ly/yF8eGu), a condizione che FooAsync restituisce un'attività, il codice seguente:

await FooAsync();
RestOfMethod();

è trasformato dal compilatore in un equivalente approssimativo di questo:

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

Guardando il codice esteso, sembra che ci sono almeno due punti di estensione che potrebbero consentire per l'acquisizione di informazioni di causalità: TaskScheduler e SynchronizationContext. Infatti, entrambi offrono simili coppie di metodi virtuali dove dovrebbe essere possibile catturare segmenti dello stack di chiamata nel momento giusto: QueueTask/TryDequeue su TaskScheduler e Post/OperationStarted su SynchronizationContext.

Purtroppo, è possibile sostituire solo TaskScheduler predefinito quando pianificare in modo esplicito un delegato tramite l'API di TPL, come Task.Run, ContinueWith, TaskFactory e così via. Questo significa che ogni volta che la continuazione è previsto di fuori di un'attività in esecuzione, il valore predefinito TaskScheduler sarà in vigore. Così, il TaskScheduler -­approccio basato non sarà in grado di acquisire le informazioni necessarie.

Per quanto riguarda SynchronizationContext, sebbene sia possibile eseguire l'override dell'istanza predefinita di questa classe per il thread corrente chiamando il Metodo SetSynchronizationContext, questo deve essere fatto per ogni thread dell'applicazione. Così, dovete essere in grado di controllo filo tutta la vita, che è praticamente impossibile se non si intende reimplementare un pool di thread. Inoltre, Windows Forms, Windows Presentation Foundation (WPF) e ASP.NET forniscono le proprie implementazioni di SynchronizationContext oltre a SynchronizationContext.Default, che gli orari di lavoro al pool di thread. Quindi, l'implementazione avrebbe dovuto comportarsi in modo diverso a seconda dell'origine del thread in cui si sta lavorando.

Si noti inoltre che quando in attesa di un awaitable personalizzato, è interamente a implementazione se utilizzare SynchronizationContext per pianificare una continuazione.

Per fortuna, ci sono due punti di estensione adatta per il nostro scenario: la sottoscrizione a eventi TPL senza dover modificare il codebase esistente, o esplicitamente optando modificando leggermente ogni attendono espressione nell'applicazione. Il primo approccio funziona solo in applicazioni desktop .NET, mentre la seconda può ospitare Windows Store apps. Sarete entrambi nelle seguenti sezioni in dettaglio.

Introduzione EventSource

.NET Framework supporta Event Tracing for Windows (ETW), dopo aver definito i provider di eventi per praticamente ogni aspetto del runtime (bit.ly/VDfrtP). In particolare, TPL genera eventi che consentono di rilevare attività durata. Anche se non tutti questi eventi sono documentati, è possibile ottenere le loro definizioni te scavando in mscorlib. dll con un tool come ILSpy o riflettore o sbirciando nella fonte di riferimento del quadro (bit.ly/HRU3) e cercare la classe TplEtwProvider. Naturalmente, il disclaimer di riflessione usuale si applica: Se le API non è documentato, non non c'è nessuna garanzia che comportamento osservato empiricamente sarà trattenuta nella prossima release.

TplEtwProvider eredita da System.Diagnostics.Tracing.EventSource, che fu introdotto nel .NET Framework 4.5 ed è ora un modo consigliato per eventi ETW in applicazione del fuoco (precedentemente si doveva trattare con ETW manifesto generazione manuale). Inoltre, EventSource permette per il consumo degli eventi nel processo, sottoscrivendo via EventListener, anche nuovo nel .NET Framework 4.5 (più su questo momentaneamente).

Il provider di eventi può essere identificato da un nome o il GUID. Ogni tipo di evento particolare è a sua volta identificato dall'ID evento e, facoltativamente, una parola chiave per distinguere da altri tipi indipendenti di eventi generati da questo provider (TplEtwProvider non utilizzare parole chiave). Ci sono parametri opzionali Task e Opcode che potreste trovare utili per il filtraggio, ma io sarò basarsi unicamente su ID evento. Ogni evento, inoltre, definisce il livello di verbosità.

TPL eventi hanno una varietà di usi oltre a catene di causalità, come il monitoraggio delle attività in volo, telemetria e così via. Essi non fuoco per awaitables personalizzato, però.

Introduzione EventListener

Il .NET Framework 4, al fine di acquisire eventi ETW, si doveva essere in esecuzione un ascoltatore ETW out-of-process, come la registrazione delle prestazioni di Windows o di Vance Morrison PerfView e quindi correlare dati acquisiti con lo stato che osservano nel debugger. Questo pone ulteriori problemi, come i dati vengono archiviati di fuori dello spazio di memoria del processo e crash dump non includerlo, che ha reso questa soluzione meno adatta per il debug post-mortem. Ad esempio, se si fa affidamento su segnalazione errori di Windows per fornire le discariche, non sarà possibile ottenere qualsiasi traccia ETW e così causalità informazioni saranno mancanti.

Tuttavia, a partire dal .NET Framework 4.5, è possibile sottoscrivere gli eventi TPL (e altri eventi generati dagli eredi EventSource) via System.Diagnostics.Tracing.EventListener (bit.ly/XJelwF). Questo permette l'acquisizione e la conservazione dei segmenti traccia dello stack in spazio di memoria del processo. Pertanto, un minidump con heap dovrebbe essere sufficiente per estrarre informazioni di causalità. In questo articolo, I'll solo la sottoscrizioni basate su EventListener di dettaglio.

Vale la pena ricordare che il vantaggio di un ascoltatore di out-of-process è che si possono sempre ottenere gli stack di chiamata attraverso l'ascolto di eventi ETW Stack (basandosi su uno strumento esistente o facendo stack noioso camminare e indirizzo modulo tracking yourself). Quando si sottoscrive gli eventi utilizzando EventListener, si possono ottenere informazioni sullo stack di chiamata in Windows Store apps, perché le API StackTrace è vietata. (Un approccio che funziona per Windows Store apps è descritto più avanti.)

Per sottoscrivere gli eventi, è necessario ereditare dall'evento­Listener, eseguire l'override del metodo OnEventSourceCreated e assicurarsi che ottiene creata un'istanza del vostro ascoltatore in ogni dominio applicazione del vostro programma (abbonamento è per ogni dominio di applicazione). Dopo la creazione di un'istanza di EventListener questo metodo verrà chiamato per notificare l'ascoltatore di origini eventi che vengono creati. Esso fornirà anche le notifiche per tutte le fonti di evento che esistevano prima che l'ascoltatore è stato creato. Dopo il filtraggio origini eventi mediante il nome o il GUID (saggio, confrontando i GUID è un'idea migliore), una chiamata a EnableEvents sottoscrive l'ascoltatore alla 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);
}

Per elaborare gli eventi, è necessario implementare il metodo astratto OnEventWritten. Ai fini della conservazione e ripristino di segmenti traccia dello stack, è necessario acquisire lo stack di chiamate a destra prima di un'operazione asincrona viene pianificata e poi, quando inizia l'esecuzione, associare un segmento traccia dello stack memorizzati con esso. Per correlare questi due eventi, è possibile utilizzare il parametro TaskID. I parametri passati a un metodo di cottura evento corrispondente in un'origine eventi sono boxed in un insieme di sola lettura di oggetti e passati come la proprietà Payload di EventWrittenEventArgs.

Interessante, ci sono speciali percorsi veloci per EventSource eventi che sono consumati come ETW (non via EventListener), dove la boxe non si verifica per loro argomenti. Questo fornisce un miglioramento delle prestazioni, ma principalmente è azzerato a causa di cross-process machinery.

Nel metodo OnEventWritten, è necessario distinguere tra fonti di evento (nel caso che ti iscrivi a più di uno) e identificare l'evento stesso. La traccia dello stack sarà catturata (archiviate) quando vengono generati eventi TaskScheduled o TaskWaitBegin e associato a un'operazione asincrona appena iniziata (restaurata) in TaskWaitEnd. È necessario inoltre passare in taskId come l'identificatore di correlazione. Figura 1 mostrata la struttura di come gli eventi saranno gestiti.

Figura 1 gestione di eventi TPL nel metodo 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: Valori espliciti ("numeri magici") nel codice sono una cattiva pratica programmazione e vengono qui utilizzati solo per brevità. Il progetto di codice di esempio accompagnamento li ha convenientemente strutturati in costanti ed enumerazioni per evitare la duplicazione e il rischio di errori di battitura.

Si noti che in TaskWaitBegin, verificare per TaskWaitBehavior essere sincroni, il che accade quando un'attività viene attesa viene eseguita in modo sincrono o è già stata completata. In questo caso, uno stack di chiamata sincrona è ancora al suo posto, quindi non ha bisogno di essere conservati in modo esplicito.

Async-Local Storage

Qualunque struttura dati si sceglie di preservare i segmenti dello stack di chiamata richiede le seguenti qualità: Valore memorizzato (catena di causalità) deve essere conservati per ogni operazione asincrona, seguente flusso di controllo lungo la strada attraverso aspettano i confini e le continuazioni, tenendo presente che le continuazioni possono eseguire su thread diversi.

Questo suggerisce una thread-local-come variabile che vuoi preservare il valore relative all'operazione asincrona corrente (una catena di continuazioni), invece di un particolare thread. Esso può essere approssimativamente denominato "async-local storage."

Il CLR dispone già di una struttura di dati chiamata ExecutionContext che ha catturato in un unico thread e restaurato da altro (dove la continuazione ottiene da eseguire), quindi essendo passati insieme al flusso di controllo. Questo è essenzialmente un contenitore che memorizza altri contesti (SynchronizationContext, CallContext e così via) che potrebbero essere necessario proseguire l'esecuzione nello stesso ambiente, dove essi sono stati interrotti. Stephen Toub ha i dettagli a bit.ly/M0amHk. Soprattutto, è possibile memorizzare dati arbitrari in CallContext (chiamando i metodi statici LogicalSetData e LogicalGetData), che sembra soddisfare lo scopo suddetto.

Tenete a mente che CallContext (in realtà, internamente ci sono due di loro: LogicalCallContext e IllogicalCallContext) è un oggetto pesante, progettato per attraversare i confini remoti. Quando nessun dati personalizzato viene memorizzati, il runtime non inizializza i contesti, risparmiando il costo del loro mantenimento con il flusso di controllo. Come si chiama il metodo LogicalSetData, una classe ExecutionContext static e diverse tabelle hash devono essere creati e passato lungo o clonato da allora in poi.

Purtroppo, ExecutionContext (insieme a tutti i suoi costituenti) viene catturato prima che il fuoco di eventi descritto TPL e ripristinato poco dopo. Così, qualsiasi dati personalizzati salvati in CallContext in mezzo viene scartati dopo ExecutionContext viene ripristinato, che lo rende inadatto per il nostro scopo particolare.

Inoltre, la classe CallContext non è disponibile nel sottoinsieme .NET per Windows Store apps, è necessario quindi un'alternativa per questo scenario.

Un modo per costruire un'archiviazione locale di async che vuoi ovviare a questi problemi è quello di mantenere il valore in memoria locale di thread (TLS), mentre la porzione sincrona del codice è in esecuzione. Quindi, quando viene generato l'evento TaskWaitStart, memorizzare il valore in un dizionario condiviso (non TLS), adattato dal TaskID. Quando viene generato l'evento di controparte, TaskWaitEnd, rimuovere il valore conservato dal dizionario e salvarlo nuovamente a TLS, possibilmente su un thread diverso.

Come sapete, i valori memorizzati in TLS sono conservati anche dopo un thread viene restituito al pool di thread e ottiene nuovi lavori da eseguire. Così, ad un certo punto, il valore deve essere rimosso da TLS (in caso contrario, alcune altre operazione asincrona eseguendo su questo thread più tardi potrebbe accedere al valore archiviato dall'operazione precedente come se fosse il proprio). Non si può fare questo nel gestore eventi TaskWaitBegin perché, in caso di nidificati attende, si verificano gli eventi TaskWaitBegin e TaskWaitEnd più volte, una volta l'attesa, e un valore archiviato potrebbe essere necessario in mezzo, come nel frammento seguente:

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

Invece, è sicuro di prendere in considerazione che il valore in TLS è idoneo a essere cancellati quando l'operazione asincrona corrente non più viene eseguito su un thread. Perché il CLR non ha un'in-­evento di processo che comunica di un thread viene riciclato torna al pool di thread (c'è un ETW uno —bit.ly/ZfAWrb), a questo scopo io uso ThreadPoolDequeueWork licenziato da FrameworkEventSource (anche irregolari), che si verifica quando viene avviata una nuova operazione su un thread di pool di thread. Questo lascia fuori non in pool di thread, per cui si avrebbe dovuto pulire manualmente il TLS, come quando un thread dell'interfaccia utente restituisce per il ciclo di messaggi.

Per un'implementazione funzionante di questo concetto insieme a segmenti dello stack catturando e concatenazione, fare riferimento alla classe StackStorage nel download del codice sorgente. C'è anche una pulita astrazione, AsyncLocal <T>, che consente di archiviare qualsiasi valore e trasferirlo con il flusso di controllo successive continuazioni asincrone. Io lo uso come archivio di catena di causalità per gli scenari di applicazioni Windows Store.

Analisi di causalità in Windows Store Apps

L'approccio descritto sarebbe ancora reggono in uno scenario di Windows Store se l'API System.Diagnostics.StackTrace erano disponibile. Per meglio o in peggio, non lo è, il che significa che non è possibile ottenere tutte le informazioni sulla chiamata stack frame sopra quella corrente dall'interno del codice. Così, anche mentre ancora sono supportati gli eventi TPL, una chiamata a TaskWaitStart o TaskWaitEnd è sepolto nel profondo le chiamate al metodo quadro, così non si dispone di alcuna informazione circa il codice che ha causato questi eventi al fuoco.

Per fortuna, .NET per Windows Store apps (così come il .NET Framework 4.5) fornisce CallerMemberNameAttribute (bit.ly/PsDH0p) ed i suoi omologhi, CallerFilePathAttribute e CallerLine­NumberAttribute. Quando gli argomenti del metodo opzionale sono decorati con questi, il compilatore inizializzerà gli argomenti con i corrispondenti valori in fase di compilazione. Ad esempio, il codice seguente sarà uscita "Main () in c:\Full\Path\To\Program.cs alla riga 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);
}

Questo permette solo il metodo di registrazione ottenere informazioni su chiamata frame, che significa che dovete assicurare che venga chiamato da tutti i metodi che si desidera catturati nella catena di causalità. Una posizione comoda per questo vuoi decorare ciascuno attendono espressione con una chiamata a un metodo di estensione, come questo:

await WorkAsync().WithCausality();

Qui, il WithCausality Metodo cattura il frame corrente, lo aggiunge alla catena di causalità e restituisce un compito o awaitable (a seconda di ciò che restituisce WorkAsync), che al completamento di quella originale rimuove il telaio dalla catena di causalità.

Come possono essere atteso più cose diverse, ci dovrebbe essere più overload di WithCausality. Questo è semplice per un'attività <T> (e anche più facile per un compito):

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

Tuttavia, è più complicato per awaitables personalizzata. Come sapete, il compilatore c# consente di attendere un'istanza di qualsiasi tipo che segue un modello particolare (vedere bit.ly/AmAUIF), che rende gli overload di scrittura che vuoi ospitare qualsiasi personalizzato awaitable Impossibile utilizzando statico digitare solo. Si possono fare alcuni overload di collegamento per awaitables predefiniti nel quadro, ad esempio YieldAwaitable o ConfiguredTaskAwaitable — o quelli definiti nella soluzione, ma in generale è necessario ricorrere a Dynamic Language Runtime (DLR). Gestione di tutti i casi richiede un sacco di codice boilerplate, quindi sentitevi liberi di guardare nel codice sorgente d'accompagnamento per i dettagli.

Vale anche la pena rilevando che in caso di nidificato attende, WithCausality metodi verranno eseguiti dall'interno all'esterno (come attendono le espressioni vengono valutate), quindi si deve prestare attenzione per assemblare lo stack nell'ordine corretto.

Catene di causalità di visualizzazione

Entrambi gli approcci descritti mantenere informazioni di causalità nella memoria come liste di segmenti dello stack di chiamata o cornici. Tuttavia, camminando li e concatenazione in una catena di causalità singolo per la visualizzazione è noioso da fare a mano.

L'opzione più semplice per automatizzare questo è sfruttare l'analizzatore di debugger. In questo caso, si crea una proprietà statica pubblica (o metodo) su una classe pubblica, che, quando viene chiamato, l'elenco dei segmenti memorizzati e restituisce una catena di causalità concatenati. Poi si può valutare questa proprietà durante il debug e vedere il risultato nel visualizzatore di testo.

Purtroppo, questo approccio non funziona in due situazioni. Uno si verifica quando il frame dello stack in primo piano è in codice nativo, che è abbastanza uno scenario comune per il debug di applicazioni si blocca, come primitive di sincronizzazione basato sul kernel chiamata in codice nativo. L'analizzatore di debugger sarebbe solo visualizzare, "Non è possibile valutare espressione perché il codice del metodo corrente è ottimizzato" (Mike Stall descrive queste limitazioni in dettaglio a bit.ly/SLlNuT).

L'altro problema è con il debug post-mortem. In realtà è possibile aprire un minidump in Visual Studio e, sorprendentemente (dato che non non c'è nessun processo di debug, solo il dump della memoria), si è permesso di esaminare i valori delle proprietà (Esegui Getter proprietà) e chiamare anche alcuni metodi! Questo incredibile pezzo di funzionalità è costruito in Visual Studio debugger e opere interpretando un'espressione Guarda e tutti i metodi che chiama (in contrasto con il debug dal vivo, dove viene eseguito il codice compilato).

Ovviamente, ci sono limitazioni. Per esempio, mentre si fa il debug dump, si non può in alcun modo chiamata in metodi nativi (cioè non è possibile eseguire anche un delegato, perché il metodo Invoke è generato nel codice nativo) o accesso alcuni limitati API (ad esempio System. Reflection). Valutazione basata su interprete è anche expectedly lento — e, purtroppo, a causa di un bug, il timeout di valutazione per il debug dump è limitato a 1 secondo in Visual Studio 2012, indipendentemente dalla configurazione. Questo, dato il numero di chiamate al metodo necessaria per attraversare l'elenco dei segmenti traccia dello stack e scorrere tutti i fotogrammi, proibisce l'uso del valutatore per questo scopo.

Per fortuna, il debugger sempre consente l'accesso ai valori di campo (anche in debug dump o quando la cornice superiore dello stack è in codice nativo), che rende possibile a strisciare attraverso gli oggetti che costituiscono una catena di causalità memorizzati e ricostruirla. Questo è ovviamente noioso, così ho scritto un'estensione di Visual Studio che fa per voi (vedere codice di esempio di accompagnamento). Figura 2 Mostra l'aspetto dell'esperienza finale. Si noti che il grafico a destra viene anche generato da questa estensione e rappresenta l'equivalente asincrono di stack in parallelo.

Causality Chain for an Asynchronous Method and “Parallel” Causality for All Threads
Figura 2 catena di causalità per un metodo asincrono e causalità "Parallelo" per tutte le discussioni

Confronto e avvertimenti

Entrambi gli approcci di causalità-rilevamento non sono liberi. La seconda (chiamante-info-based) è più leggero, non coinvolgono il costoso API StackTrace, affidandosi invece il compilatore per fornire chiamante informazioni telaio durante la fase di compilazione, che significa "libera" in un programma in esecuzione. Tuttavia, utilizza ancora infrastruttura di gestione degli eventi con relativo costo a sostegno AsyncLocal <T>. D'altra parte, il primo approccio fornisce ulteriori dati, non saltando fotogrammi senza aspetta. Rintraccia automaticamente anche molte altre situazioni dove il Task-based asincronia presenta senza attendono, come ad esempio il metodo Task.Run; d'altra parte, non funziona con awaitables personalizzati.

Un ulteriore vantaggio del tracker TPL basati su eventi è che codice asincrono esistente non deve essere modificato, mentre per il chiamante informazioni basate su attributi approccio, è necessario modificare ogni attendono istruzione nel vostro programma. Ma solo quest'ultimo supporta Windows Store apps.

Il tracker di eventi TPL soffre anche un sacco di codice boilerplate quadro in segmenti traccia dello stack, anche se esso può essere facilmente filtrato dal nome dello spazio dei nomi o classe frame. Vedere il codice di esempio per un elenco di filtri comuni.

Un altro avvertimento riguarda loop in codice asincrono. Si consideri il seguente frammento:

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

Entro la fine del metodo, la catena di causalità sarebbe cresciuto a più di 30 segmenti, alternando ripetutamente telai FirstAsync, SecondAsync e ThirdAsync. Per un ciclo finito, questo può essere tollerabile, anche se ancora è uno spreco di memoria per memorizzare i fotogrammi duplicati 10 volte. Tuttavia, in alcuni casi, un programma potrebbe introdurre un ciclo infinito valido, ad esempio, nel caso di un ciclo di messaggi. Inoltre, ripetizione infinita potrebbe essere introdotta senza ciclo o attendono costrutti — un timer riprogrammazione stessa su ogni tick è un perfetto esempio. Una catena di causalità infinita di rilevamento è un modo sicuro a corto di memoria, quindi la quantità di dati memorizzati deve essere ridotta a una quantità finita in qualche modo.

Questo problema non influisce sul tracker basati su informazioni chiamante, come esso rimuove un frame dall'elenco immediatamente dopo l'inizio di una continuazione. Ci sono due approcci (cumulabili) per risolvere questo problema per lo scenario di eventi TPL. Uno è quello di tagliare i vecchi dati basati sulla quantità massima di memoria rotolamento. L'altro è di rappresentare loop in modo efficiente ed evitare duplicazioni. Per entrambi gli approcci, si potrebbe anche rilevare modelli comuni di loop infinito e tagliare la catena di causalità in modo esplicito in questi punti.

Sentitevi liberi di fare riferimento al progetto accompagnamento campione per vedere come il ciclo pieghevole potrebbe essere implementato.

Come dichiarato, il TPL eventi API soltanto permette di catturare una catena di causalità, non un grafico. Questo è perché i metodi WaitAll e Task.WhenAll vengono implementati come conti alla rovescia, dove la continuazione è prevista solo quando l'ultimo compito viene completato e il contatore raggiunge lo zero. Così, solo l'ultima operazione completata forma una catena di causalità.

Conclusioni

In questo articolo, che hai imparato la differenza tra uno stack di chiamate, una pila di ritorno e una catena di causalità. Ora dovreste essere a conoscenza dei punti di estensione che fornisce il Framework .NET per tenere traccia di pianificazione ed esecuzione di operazioni asincrone ed essere in grado di sfruttare queste per catturare e conservare le catene di causalità. Gli approcci descritti copertura tracking causalità nel classico e Windows Store apps, entrambi in diretta e post-mortem scenari di debug. Imparato anche il concetto di archiviazione locale di async e sua possibile implementazione per Windows Store apps.

Ora si può andare avanti e incorporare il rilevamento nel vostro codice asincrono della causalità o utilizzare async-local storage nei calcoli paralleli; esplorare le origini di eventi che il .NET Framework 4.5 e .NET per Windows Store apps offrono di costruire qualcosa di nuovo, come un tracker per compiti incompiuti nel programma; o utilizzare questo punto di estensione a fuoco i propri eventi per ottimizzare le prestazioni dell'applicazione.

Andriy (Andrew) Stasyuk è un software development engineer in test II nel team linguaggi gestiti presso Microsoft. Ha sette anni di esperienza come un partecipante, autore del compito, membro della giuria e allenatore a vari concorsi nazionali e internazionali di programmazione. Ha lavorato nello sviluppo di software finanziari presso Paladyne/Broadridge Financial Solutions Inc. e Deutsche Bank AG prima di passare a Microsoft. I suoi principali interessi nella programmazione sono algoritmi, parallelismo e rompicapo.

Grazie ai seguenti esperti tecnici per la revisione di questo articolo: Vance Morrison e Lucian Wischik