Dieser Artikel wurde maschinell übersetzt.

Asynchrone Programmierung

Asynchrone Nachverfolgung von Kausalitätsketten

Andrew Stasyuk

 

Mit dem Aufkommen von C# 5, Visual Basic .NET 11, die Microsoft .NET Framework 4.5 und .NET für Windows Store apps, die asynchrone Programmierung Erfahrung wurde erheblich gestrafft.Neue Async und warten auf Schlüsselwörter (Async und Await in Visual Basic) ermöglichen es Entwicklern, die gleiche Abstraktion pflegen sie gewohnt, beim synchronen Code zu schreiben waren.

Viel Aufwand wurde in Visual Studio 2012 zur Verbesserung der asynchronen Debuggen mit Tools wie Parallel Stacks, parallele Tasks, parallele Watch und die Parallelität Schnellansicht genommen.Jedoch sind in Bezug auf die als auf einer Stufe mit den synchronen Code Debuggen Erfahrung, wir durchaus noch nicht.

Die wichtigere Probleme, bricht die Abstraktion und offenbart interne Sanitär hinter der Fassade Async/erwarten, gehört die mangelnde Aufruflisteninformationen im Debugger.In diesem Artikel werde ich ermöglichen diese Kluft zu überbrücken und Verbesserung der asynchronen Debugvorgang in Ihrer .NET 4.5 oder Windows-Speicher-app.

Wir setzen sich zunächst auf wesentliche Terminologie.

Definition einer Aufrufliste

MSDN-Dokumentation (bit.ly/Tukvkm) verwendet, um die Aufrufliste definieren als "die Reihe Methodenaufrufe, die von Anfang des Programms nach der Anweisung, die derzeit zur Laufzeit ausgeführt wird." Diese Vorstellung war perfekt für das Single-Threading, synchrones Programmiermodell gültig, aber jetzt, Parallelität und asynchrone an Dynamik gewinnt, präziser Taxonomie ist notwendig.

Im Sinne dieses Artikels ist es wichtig, die Kausalität-Kette vom Stapel zurück zu unterscheiden.Innerhalb des synchronen Paradigmas, diese beiden Begriffe sind meist identisch (ich werde den Ausnahmefall später erwähnt).Im asynchronen Code beschreibt die oben genannten Definition eine Kette der Kausalität.

Auf der anderen Seite wird die derzeit ausgeführte Anweisung an, wenn fertig, eine Reihe von Methoden, die weiterhin ihre Ausführung führen.Diese Reihe bildet den Rückkehr Stack.Alternativ für den Leser vertraut mit der Fortsetzung übergeben Stil (Eric Lippert hat eine fabelhafte Serie zu diesem Thema ab bit.ly/d9V0Dc), der Rückkehr Stapel definiert werden als eine Reihe von Fortsetzungen, die zum Ausführen, sollten die derzeit ausgeführte Methode fertig registriert sind.

Kurzum, beantwortet die Kette der Kausalität die Frage: "Wie habe ich hier?" während return-Stack ist die Antwort, "wo ich weiter gehen?" Beispielsweise wenn Sie einen Deadlock in Ihrer Anwendung haben, können Sie möglicherweise herausfinden, was es von der ehemaligen verursacht, während Letzteres Sie wissen lasse, was die Konsequenzen sind.Beachten Sie, dass während eine Kausalität-Kette immer wieder auf den Programm-Einstiegspunkt verfolgt, Rückkehr Stack an dem Punkt abgeschnitten ist, wo das Ergebnis des asynchronen Vorgangs (z. B. Async-void-Methoden oder Fraktionsgremien über ThreadPool.QueueUserWorkItem) nicht eingehalten werden.

Es gibt auch eine Vorstellung von Stack-Trace wird eine Kopie eines synchronen Aufruf-Stack für die Diagnose erhalten; Ich werde diese beiden Begriffe synonym verwenden.

Beachten Sie, dass gibt es mehrere unausgesprochene Annahmen in den obigen Definitionen:

  • "Methodenaufrufe" bezeichnet in der ersten Definition im Allgemeinen bedeutet "Methoden, die noch nicht abgeschlossen haben", die die physikalische Bedeutung des "Seins Stapel" in dem synchronen Programmiermodell zu tragen.Jedoch während wir in der Regel nicht Interesse an Methoden, die bereits zurückgekehrt sind, ist es nicht immer möglich, sie während des Debuggens asynchrone zu unterscheiden.In diesem Fall gibt es keinen physikalischen Begriff des "Seins Stapel" und alle Fortsetzungen sind gleichermaßen gültige Elemente einer Kette der Kausalität.
  • Auch in synchronen Code eine Kette der Kausalität und bringe nicht immer identisch.Einen besonderen Fall, wenn eine Methode in einer, aber andererseits fehlt möglicherweise ist ein Schwanz-Aufruf.Obwohl nicht direkt Ausdrücken in C# und Visual Basic .NET, kann sie codiert in Intermediate Language (IL) ("Schwanz."-Präfix) oder von der just-in-Time (JIT) Compiler (vor allem in einem 64-Bit-Prozess) produziert.
  • Last, but not least Kausalität Ketten und Stapel nichtlineare sein können.Das heißt, im allgemeinsten Fall sind sie gerichtet Graphen, die mit der aktuellen Anweisung als Senke (Kausalität Graph) oder Quelle (Rückkehr Graph).Nichtlinearität im asynchronen Code soll die Gabeln (parallele asynchrone Vorgänge aus einer) und Verknüpfungen (Fortsetzung nach Abschluss einer Reihe von parallelen asynchronen Vorgänge ausgeführt).Im Sinne dieses Artikels und Plattform bedingt (später erklärt) ich halte nur lineare Kausalität Ketten und Stapeln, die Teilmengen der entsprechenden Diagramme sind zurück.

Glücklicherweise ist, wenn asynchrone wird eingeführt in ein Programm mithilfe von Async Schlüsselwörter ohne Gabeln oder Verknüpfungen zu erwarten und alle asynchronen Methoden werden erwartet, die Kette der Kausalität noch identisch mit der Rückkehr Stack, gerade wie synchrone Code.In diesem Fall sind beide gleichermaßen nützlich orientieren Sie sich in der Ablaufsteuerung.

Auf der anderen Seite sind die Kausalität Ketten selten gleich zurückgeben, Felsnadeln Programme explizit mit Fortsetzungen, ein bemerkenswertes Beispiel, wird der Task Parallel Library (TPL) Dataflow geplant.Dies ist aufgrund der Art der Daten fließen aus einer Quelle-Block auf ein Ziel-Block, nie wieder in der ehemaligen.

Vorhandene Tools

Betrachten Sie ein kleines Beispiel:

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

Durch Extrapolation der Abstraktion, die Entwickler zum synchronen Debuggen verwendet wurden, erwarten sie folgende Kausalität Kette/Rückkehr Stapel zu sehen, wenn die Ausführung bei der Console.WriteLine-Methode angehalten wurde:

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

Aber wenn Sie dies versuchen, werden Sie feststellen, dass im Fenster Aufrufliste die Main-Methode fehlt, während die Stapelüberwachung direkt in der OperationAsync-Methode vorangestellt [Wiederaufnahme Async Methode] beginnt. Parallele Stapel hat beide Methoden; Es zeigt jedoch nicht, dass Main OperationAsync aufruft. Parallele Tasks hilft nicht, entweder mit "Keine Tasks angezeigt."

Hinweis: Der Debugger ist an dieser Stelle bewusst die Main-Methode, die als Teil der Aufrufliste — Sie vielleicht bemerkt haben, die durch den grauen Hintergrund hinter dem Aufruf von OperationAsync. Die CLR und Windows Runtime (WinRT) müssen wissen, wo Sie die Ausführung fortsetzen, nachdem der oberste Stapelrahmen zurückgegeben; So speichern sie tatsächlich Rückkehr Stapel. In diesem Artikel werden jedoch nur Kausalität tracking, verlassen Rückkehr Stapel als Thema für einen anderen Artikel geht.

Erhaltung der Kausalität Ketten

Kausalität-Ketten sind in der Tat nie von der Laufzeit gespeichert. Auch Sie beim Debuggen von Code synchron sind sehen, im wesentlichen Aufruflisten zurückgeben Stapel — wie eben gesagt wurde, sie sind notwendig für die CLR und Windows Runtime zu wissen, welche Methoden auszuführen, nachdem der oberste Rahmen zurückgibt. Die Common Language Runtime muss nicht wissen, was verursacht eine bestimmte Methode auszuführen.

Um die Kausalität Ketten während Leben und Post-Mortem debugging anzeigen zu können, müssen Sie explizit sie auf dem Weg zu bewahren. Vermutlich würde dies erfordern, speichern (synchron) Stack-Trace-Informationen an jedem Punkt, wo die Fortsetzung geplant ist, und diese Daten wiederherstellen, wenn Fortsetzung beginnt zu führen. Diese stack Trace, die Segmente dann in Form einer Kausalität-Kette zusammengefügt werden konnten.

Wir sind mehr daran interessiert, die Kausalität Informationsübermittlung über erwarten Konstrukte, wie das ist, wo die Abstraktion der Ähnlichkeit mit synchronen Code bricht. Mal sehen, wie und wann diese Daten erfasst werden können.

Wie Stephen Toub (bit.ly/yF8eGu), sofern die FooAsync eine Aufgabe, den folgenden Code gibt:

await FooAsync();
RestOfMethod();

wird durch den Compiler eine grobe Entsprechung dieser umgewandelt:

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

Aus den erweiterten Code betrachten, scheint es, gibt es mindestens zwei Erweiterungspunkte, die zum Erfassen von Kausalität Informationen ermöglichen könnte: TaskScheduler und SynchronizationContext.In der Tat bieten beide ähnliche Paare von virtuellen Methoden Aufruf-Stack-Segmente auf die richtigen Momente zu erfassen werden sollten: QueueTask/TryDequeue auf TaskScheduler und Post/OperationStarted zu SynchronizationContext.

Leider können Sie nur Standard TaskScheduler ersetzen, wenn Sie explizit ein Delegat über die TPL-API, wie Task.Run, Task.ContinueWith, TaskFactory.StartNew und so weiter planen.Dies bedeutet, dass bei jedem Fortsetzung außerhalb einer ausgeführten Aufgabe geplant ist, die Standardeinstellung TaskScheduler in Kraft sein wird.Also der TaskScheduler -­Ansatz wird nicht in der Lage, die notwendigen Informationen zu erfassen werden.

Was SynchronizationContext obwohl es möglich, die Standardinstanz dieser Klasse für den aktuellen Thread durch Aufrufen der SynchronizationContext.SetSynchronizationContext-Methode zu überschreiben ist, hat dies für jeden Thread in der Anwendung erfolgen.Daher müssten Sie Steuerelement Thread Lebensdauer, können das ist unmöglich, wenn Sie nicht planen, einen Threadpool neu implementieren.Darüber hinaus bieten Windows Forms, Windows Presentation Foundation (WPF) und ASP.NET eigene Implementierungen von SynchronizationContext neben SynchronizationContext.Default, die Arbeit an den Threadpool plant.Daher müssten Ihre Implementierung unterschiedlich je nach Herkunft des Threads Verhalten in dem es funktioniert.

Auch beachten Sie, dass, wenn eine benutzerdefinierte awaitable zu erwarten, ist es vollständig bis zur Implementierung, ob SynchronizationContext zu verwenden, um eine Fortsetzung zu planen.

Glücklicherweise gibt es zwei Erweiterungspunkten geeignet für unser Szenario: TPL Ereignisse abonnieren, ohne dass Sie die vorhandenen Codebasis zu ändern, oder explizit durch leicht ändern Opt-in jeden Ausdruck in der Anwendung erwarten.Der erste Ansatz funktioniert nur in .NET-desktop-Anwendungen, während die zweite Windows Store apps unterbringen kann.Ich werde beide in den folgenden Abschnitten ausführlich.

Einführung EventSource

Das .NET Framework unterstützt Event Tracing für Windows (ETW), nach der Definition Ereignisanbieter für praktisch jeden Aspekt der Runtime (bit.ly/VDfrtP).Insbesondere löst TPL Ereignisse, mit denen Sie Aufgabe Lebensdauer verfolgen.Obwohl nicht alle diese Ereignisse dokumentiert sind, erhalten Sie ihre Definitionen sich durch Eintauchen in mscorlib.dll mit einem Tool wie ILSpy oder Reflektor oder spähen in Framework-Referenz-Quelle (referencesource.microsoft.com/) und der Suche nach der TplEtwProvider-Klasse.Natürlich gilt der übliche Reflexion-Haftungsausschluss: Wenn die API ist nicht dokumentiert, gibt es keine Garantie, dass empirisch beobachtete Verhalten in der nächsten Version beibehalten wird.

TplEtwProvider von System.Diagnostics.Tracing.EventSource, die in das .NET Framework 4.5 eingeführt wurde und ist jetzt eine empfohlene Methode zum Feuer-ETW-Ereignisse in Ihrer Anwendung erbt (zuvor hatte Sie befassen sich mit ETW Manifestgenerierung Handbuch).Darüber hinaus ermöglicht EventSource Verbrauch der Ereignisse im Prozess, indem sie über EventListener, ebenfalls neu in das .NET Framework 4.5 (mehr dazu momentan) abonnieren.

Ereignisanbieter kann durch einen Namen oder die GUID identifiziert werden.Jedes bestimmten Ereignistyp wird wiederum durch Ereigniskennung und optional ein Schlüsselwort zu unterscheiden von anderen unabhängigen Ereignissen dieser Anbieter (TplEtwProvider nicht Schlüsselwörter verwenden) identifiziert.Es gibt optionale Aufgabe und Opcode-Parameter, die Sie zum Filtern nützlich finden könnten, aber ich werde verlassen sich ausschließlich auf die Ereignis-IDJedes Ereignis definiert auch die Stufe der Ausführlichkeit.

TPL Ereignisse verfügen über eine Vielzahl von Einsatzmöglichkeiten neben Kausalität-Ketten, z. B. verfolgen Aufgaben während des Flugs, Telemetrie und so weiter.Sie Feuer nicht für benutzerdefinierte Awaitables, obwohl.

Einführung EventListener

.NET Framework 4 musste um ETW-Ereignissen, ausgeführt einen Out-of-Process-ETW-Listener, z. B. Windows Performance Recorder oder Vance Morrison des PerfView, und dann korrelieren erfasste Daten mit den Zustand, den Sie im Debugger beobachtet.Dies bedingte zusätzliche Probleme, da Daten außerhalb Prozess Speicherplatz gespeichert wurde und Crash-Dumps nicht, die diese Lösung weniger geeignet enthalten für Post-Mortem debugging gemacht.Beispielsweise wenn Sie auf Windows-Fehlerberichterstattung zu Deponien verlassen, erhalten Sie keine ETW Spuren und somit werden Kausalität Informationen fehlen.

Die .NET Framework 4.5 ab, es ist jedoch möglich TPL Ereignisse (und anderen Ereignissen EventSource erben) über System.Diagnostics.Tracing.EventListener abonnieren (bit.ly/XJelwF).Dies ermöglicht die Erfassung und Erhaltung der Stapel Ablaufverfolgung Segmente im Speicherbereich Prozess.Daher sollte ein Minidump mit Heap Kausalität Informationen extrahieren genügen.In diesem Artikel werde ich nur Detail EventListener-basierten Abonnements.

Es ist erwähnenswert, dass der Vorteil ein Out-of-Process-Listener ist, dass man immer die Aufruflisten können durch das hören auf die Stack-ETW-Ereignisse (unter Berufung auf ein vorhandenes Werkzeug oder mühsam Stackwalking und Moduladresse Verfolgung selbst zu tun).Wenn die Ereignisse mit EventListener abonnieren, können nicht Sie Aufruflisteninformationen in Windows Store apps, erhalten, weil die StackTrace-API ist verboten.(Ein Ansatz, der für Windows-Store apps funktioniert ist weiter unten beschrieben.)

Um Ereignisse zu abonnieren, müssen Sie von Ereignis erben­-Listener, die OnEventSourceCreated-Methode überschreiben und stellen sicher, dass eine Instanz der Listener, in jeder AppDomain Ihres Programms erstellt wird (Abonnement ist pro Anwendungsdomäne).Nachdem EventListener instanziiert wird, wird diese Methode aufgerufen werden, um den Hörer der Ereignisquellen zu benachrichtigen, die erstellt werden.Es liefert auch Benachrichtigungen für alle Ereignisquellen, die vor der Listener erstellt wurde.Nach dem Filtern der Ereignisquellen Name oder GUID (performance-wise, Vergleich von GUIDs besser ist), abonniert ein Aufruf von EnableEvents den Zuhörer auf die Quelle:

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

Um die Ereignisse zu verarbeiten, müssen Sie abstrakte Methode OnEventWritten zu implementieren. Zum Zwecke der Erhaltung und Wiederherstellung der Stack-Trace-Segmente, musst du die Aufrufliste erfassen kurz bevor eine asynchrone Operation geplant ist, und dann, wenn es die Ausführung beginnt, zuordnen eine gespeicherte Stack-Trace-Segment. Um diese beiden Ereignisse zu korrelieren, können Sie den TaskID-Parameter verwenden. An einer entsprechenden Ereignis-Brand-Methode in einer Ereignisquelle übergebenen Parameter werden in eine nur-Lese Objekt-Sammlung geschachtelt und als die Payload-Eigenschaft des EventWrittenEventArgs übergeben.

Interessanterweise gibt es spezielle schnelle Wege für EventSource-Ereignisse, die als ETW (nicht über EventListener), verbraucht werden wo Boxen nicht auftritt für ihre Argumente. Dies stellt eine Leistungsverbesserung bereit, aber es ist vor allem auf NULL raus wegen prozessübergreifende Maschinerie.

In der OnEventWritten-Methode müssen Sie unterscheiden zwischen Ereignisquellen (im Fall, dass Sie mehrere abonnieren) und das Ereignis selbst zu identifizieren. Die Stapelüberwachung wird erfasst werden (gespeichert), wenn TaskScheduled oder TaskWaitBegin-Ereignisse auslösen, und im Zusammenhang mit einer neu gestartete asynchronen Operation (wiederhergestellt) in TaskWaitEnd. Sie müssen auch TaskId als der Korrelations-ID übergeben. Abbildung 1 zeigt die Gliederung der wie die Ereignisse behandelt werden.

Abbildung 1 Behandlung von TPL-Ereignissen in der OnEventWritten-Methode

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

Hinweis: Explizite Werte ("magische Zahlen") im Code sind von einem schlechten Programmierstil und dienen hier nur der Kürze halber. Das zugehörigen Code-Beispielprojekt hat sie bequem in Konstanten und Enumerationen zur Vermeidung von Doppelarbeit und Risiko von Tippfehlern strukturiert.

Beachten Sie, dass in TaskWaitBegin, ich für TaskWaitBehavior wird synchron, was geschieht überprüfen, wenn eine Aufgabe erwartete wird synchron ausgeführt wird oder bereits abgeschlossen. In diesem Fall ist ein synchroner Aufruf-Stack noch in Kraft, so dass es nicht explizit gespeichert werden muss.

Async-Local Storage

Welcher Datenstruktur, die Aufruf-Stack-Segmente beibehalten werden sollen benötigt die folgende Qualität: Gespeicherten Wert (Kausalität Kette) sollte gewährleistet sein, denn alle asynchronen Vorgang folgende Ablaufsteuerung auf dem Weg über erwarten, Grenzen und Fortsetzungen, wenn man bedenkt, die Fortsetzungen in verschiedenen Threads ausgeführt werden können.

Dies deutet auf eine fadenähnliche-lokale Variable, die ihren Wert im Zusammenhang mit den aktuellen asynchronen Vorgang (eine Kette von Fortsetzungen), bewahren würde anstelle eines bestimmten Threads. Es kann etwa "Async lokalen Threadspeicher." benannt werden

Die CLR hat bereits eine Datenstruktur namens ExecutionContext, die hat einen Thread aufgezeichnet und wiederhergestellt andererseits (wo Fortsetzung wird ausgeführt), also zusammen mit Ablaufsteuerung übergeben wird. Dies ist im Wesentlichen ein Container, in dem anderen Kontexten (SynchronizationContext, CallContext usw.) gespeichert, die erforderlich werden könnten, weiterhin Ausführung in genau der gleichen Umgebung, wo sie unterbrochen wurden. Stephen Toub hat die Details an bit.ly/M0amHk. Am wichtigsten ist, können Sie beliebige Daten in CallContext (durch Aufrufen der statischen Methoden LogicalSetData und LogicalGetData), speichern, die zu den zuvor benannten Zweck zu entsprechen scheint.

Denken Sie daran, dass CallContext (tatsächlich intern gibt es zwei davon: LogicalCallContext und IllogicalCallContext) ist ein schweres Objekt, über Remotegrenzen hinweg fließen soll. Wenn keine benutzerdefinierten Daten gespeichert werden, initialisieren nicht die Common Language Runtime die Kontexte, Ersatz der Kosten für deren Erhaltung auch mit der Ablaufsteuerung. Sobald Sie die CallContext.LogicalSetData-Methode aufrufen, haben eine veränderbare ExecutionContext und mehrere Hashtabellen erstellt und weitergegeben oder fortan geklont werden.

ExecutionContext (zusammen mit allen seinen Bestandteilen) ist leider vor dem beschriebenen TPL-Veranstaltungen-Feuer gefangen genommen und kurz danach wiederhergestellt. Benutzerdefinierten Daten gespeichert in CallContext dazwischen ist daher verworfen, nachdem ExecutionContext wiederhergestellt wird, die es für unseren Zweck ungeeignet macht.

Darüber hinaus ist die CallContext-Klasse nicht verfügbar in der .NET für Windows Store apps Teilmenge, also eine Alternative für dieses Szenario erforderlich.

Eine Möglichkeit, einen Async-lokalen Speicher zu erstellen, der diese Probleme umgehen würde ist den Wert im lokalen Threadspeicher (TLS) beibehalten, während der synchrone Teil der Code ausgeführt wird. Dann, wenn das TaskWaitStart-Ereignis ausgelöst wird, speichern Sie den Wert in einem freigegebenen (nicht-TLS) Wörterbuch, sortiert nach der TaskID. Wenn das Gegenstück Ereignis, TaskWaitEnd, ausgelöst wird, entfernen Sie den erhaltenen Wert aus dem Wörterbuch und wieder speichern Sie TLS, möglicherweise in einem anderen Thread.

Wie Ihr wisst vielleicht, werden in TLS gespeicherten Werte beibehalten, auch nachdem ein Thread an den Threadpool zurückgegeben und wird neue Arbeit ausführen. Also, irgendwann muss der Wert von TLS entfernt werden (andernfalls einige andere asynchrone Operation ausführen auf diesem Thread später möglicherweise zugreifen den Wert von den vorhergehenden Vorgang gespeichert werden, als wären es seine eigenen). Sie können nicht dazu in der TaskWaitBegin-Ereignishandler da für verschachtelte erwartet, TaskWaitBegin TaskWaitEnd Ereignisse und mehrere Male, einmal pro Await, und ein gespeicherter Wert möglicherweise benötigten dazwischen, wie z. B. in folgenden Ausschnitt:

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

Stattdessen ist es sicher zu berücksichtigen, dass der Wert im TLS ist berechtigt, die gelöscht werden, wenn ein Thread nicht mehr der aktuelle asynchrone Vorgang ausgeführt wird. Weil die CLR keine in-­Prozessereignis, das eines Threads Recycling zurück an den Threadpool melden (gibt es eine ETW einer —bit.ly/ZfAWrb), für diesen Zweck verwende ich ThreadPoolDequeueWork von FrameworkEventSource (auch ohne Papiere), ausgelöst, das auftritt, wenn eine neue Operation auf einem Threadpoolthread gestartet wird. Dies lässt nicht gepoolt Threads, bei denen müssten Sie manuell bereinigen die TLS, z. B. Wenn ein UI-Thread in der Meldungsschleife zurückgibt.

Für eine funktionierende Umsetzung dieses Konzepts zusammen mit Stack-Segmente finden erfassen und Verkettung, die StackStorage-Klasse im Source Codedownload. Es gibt auch eine sauberere Abstraktion, AsyncLocal <T>, mit dem Sie jeden Wert speichern und übertragen Sie es mit der Ablaufsteuerung auf nachfolgenden asynchronen Fortsetzungen. Ich werde es als Kausalität Kette Speicher für Windows Store apps Szenarien verwenden.

Ablaufverfolgung Kausalität im Windows Store Apps

Der beschriebene Ansatz würde noch in einem Windows-Speicher-Szenario halten, wenn die System.Diagnostics.StackTrace-API zur Verfügung standen. Denn wohl oder übel, es ist nicht, das heißt, Sie kann keine Informationen über den Aufruf nicht Stack-Frames über die jetzige aus innerhalb des Codes erhalten. Somit sogar während TPL Ereignisse noch unterstützt werden, ist ein Aufruf von TaskWaitStart oder TaskWaitEnd tief in die Rahmen-Methodenaufrufe verborgen so dass Sie keine Informationen über Ihren Code, die diese Ereignisse haben ausgelöst verursacht.

Glücklicherweise bietet .NET für Windows Store apps (wie auch die .NET Framework 4.5) CallerMemberNameAttribute (bit.ly/PsDH0p) und seinen Kollegen CallerFilePathAttribute und CallerLine­NumberAttribute. Wenn mit diesen optionalen Methodenargumente eingerichtet sind, wird der Compiler zur Kompilierzeit Argumente mit den entsprechenden Werten initialisieren. Beispielsweise wird der folgende Code ausgeben "Main() in c:\Full\Path\To\Program.cs in Zeile 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);
}

Dadurch wird nur die Protokollierung-Methode, um Informationen über den aufrufenden Rahmen zu erhalten, was, dass Sie haben bedeutet, um sicherzustellen, dass es von allen Methoden aufgerufen wird, gefangen in der Kette der Kausalität gewünschte. Die günstige Lage für diese Dekoration werden würde jeder erwarten Ausdruck mit einem Aufruf einer Erweiterungsmethode wie folgt:

await WorkAsync().WithCausality();

Hier, die WithCausality-Methode erfasst der aktuelle Frame, hängt an der Kette der Kausalität und gibt eine Aufgabe oder awaitable (je nachdem, welche WorkAsync-Erträge), die nach Abschluss des Originals entfernt des Rahmens aus der Kette der Kausalität.

Da mehrere verschiedene Dinge erwartet werden können, sollte es mehrere Überladungen der WithCausality. Dies ist einfach eine Aufgabe <T> (und noch leichter für eine Aufgabe):

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

Allerdings ist es schwieriger für benutzerdefinierte Awaitables. Wie Ihr wisst vielleicht, der c#-Compiler können Sie eine Instanz eines beliebigen Typs erwarten, die ein bestimmtes Muster folgt (siehe bit.ly/AmAUIF), wodurch schreiben Überladungen, die alle benutzerdefinierten awaitable unmöglich mit unterbringen würde statische Typisierung nur. Sie können dabei ein paar Kontextmenü Überladungen für vordefinierte im Rahmen, z. B. YieldAwaitable oder ConfiguredTaskAwaitable Awaitables — oder die, die in der Projektmappe definiert — aber im Allgemeinen haben Sie zu der Dynamic Language Runtime (DLR) zurückgreifen. Behandlung von allen Fällen erfordert eine Menge Standardcode, so fühlen sich frei in der begleitenden Quellcode für Details schauen.

Es lohnt sich auch darauf hingewiesen, dass im Falle einer geschachtelten erwartet, WithCausality Methoden ausgeführt werden vom inneren zum äußeren (wie erwarten Ausdrücke ausgewertet), so dass darauf geachtet werden muss, um den Stapel in der richtigen Reihenfolge montieren.

Anzeige Kausalität Ketten

Beide beschriebenen Ansätze halten Kausalität Informationen im Speicher als Listen von Aufruf-Stack-Segmente oder Frames. Wandern sie und Verkettung in einer einzigen Kausalität-Kette für die Anzeige ist jedoch mühsam von hand zu tun.

Die einfachste Möglichkeit, dies zu automatisieren ist der Debugger-Evaluator nutzen. In diesem Fall Sie eine öffentliche statische Eigenschaft (oder Methode) auf eine öffentliche Klasse erstellen, die beim Aufruf führt die Liste der gespeicherten Segmente und gibt eine verkettete Kausalität-Kette. Dann können Sie diese Eigenschaft während des Debuggens zu bewerten und das Ergebnis im Text Visualizer sehen.

Leider funktioniert dieser Ansatz nicht in zwei Situationen. Man tritt auf, wenn der oberste Stapelrahmen in systemeigenem Code das durchaus ein häufiges Szenario ist für das Debuggen der Anwendung hängt, wie Kernel-basierte Synchronisierungsprimitiven in systemeigenen Code aufrufen. Der Debugger-Bewerter würde nur angezeigt, "Ausdruck kann nicht ausgewertet, weil der Code der aktuellen Methode optimiert ist" (Mike Stall beschreibt diese Einschränkungen im Detail am bit.ly/SLlNuT).

Die andere Frage ist mit Post-Mortem debugging. Sie können tatsächlich ein Minidump in Visual Studio öffnen und, überraschenderweise (da ist kein Prozess Debuggen, nur seine Speicherabbild), Sie sind erlaubt, Eigenschaftswerte (Ausführung Eigenschaftengettern) untersuchen und sogar einige Methoden aufrufen! Dieses erstaunliche Stück Funktionalität ist in der Visual Studio -Debugger und arbeitet durch die Interpretation eines Überwachungsausdrucks und alle Methoden, denen sie (im Gegensatz aufruft zu Livedebuggen, wo die kompilierter Code ausgeführt wird) integriert.

Natürlich gibt es Einschränkungen. Z. B. können nicht beim tun der Müllkippe zu debuggen, Sie in jeder Weise Aufruf native Methoden (d. h. einen Delegaten nicht selbst ausgeführt werden kann, da die Invoke-Methode in systemeigenem Code generiert wird) oder Access einige eingeschränkt APIs (z. B. System.Reflection). Interpreter-basierte Bewertung ist auch erwartungsgemäß langsam – und leider aufgrund eines Fehlers, die lauffähig für das Debuggen Dump, begrenzt auf 1 Sekunde in Visual Studio 2012, unabhängig von der Konfiguration. Angesichts die Anzahl der Methodenaufrufe, die zum Durchlaufen der Liste der Stack-Trace-Segmente und durchlaufen alle Frames, verbietet die Verwendung des Bewerters für diesen Zweck.

Zum Glück der Debugger immer ermöglicht den Zugriff auf Feldwerte (auch im Dump Debuggen oder wenn der oberen Stapelrahmen in systemeigenem Code ist), das macht es möglich, durch die Objekte bilden eine Kette von gespeicherten Kausalität kriechen und es zu rekonstruieren. Dies ist natürlich langweilig, also schrieb ich eine Visual Studio -Erweiterung, die dies für Sie erledigt (siehe Beispielcode). Abbildung 2 zeigt, was die endgültige Erfahrung aussieht. Beachten Sie, dass das Diagramm auf der rechten Seite auch von dieser Erweiterung erzeugt wird und das Async-Äquivalent von Parallel Stacks darstellt.

Causality Chain for an Asynchronous Method and “Parallel” Causality for All Threads
Abbildung 2 Kausalität Kette für eine asynchrone Methode und "Parallel" Kausalität für alle Threads

Vergleich und Vorbehalte

Beide Ansätze Kausalität-Tracking sind nicht kostenlos. Das zweite (Anrufer-Infos-basiert) ist leichter, als es nicht, dass die teuren StackTrace-API, die sich stattdessen auf den Compiler bedeuten an Aufrufer Rahmeninformationen beim Kompilieren, bereitstellen, das bedeutet "" auf ein laufendes Programm frei zu verlassen. Es verwendet jedoch noch Ereignisinfrastruktur mit seiner Kosten, um AsyncLocal <T> zu unterstützen. Auf der anderen Seite der erste Ansatz stellt weitere Daten, nicht überspringen Frames ohne erwartet. Es spürt automatisch auch mehrere andere Situationen, in denen aufgabenbasierte asynchrone, ohne entsteht, abzuwarten, wie die Task.Run-Methode; auf der anderen Seite, funktioniert es nicht mit benutzerdefinierten Awaitables.

Ein weiterer Vorteil der TPL-Veranstaltungen-basierten-Tracker ist, dass vorhandener asynchroner Code nicht geändert werden, während für den Anrufer Info Attribute beruhenden Ansatz müssen Sie ändern jede Anweisung in Ihrem Programm erwarten. Aber nur die letztere unterstützt Windows Store apps.

Der TPL-Veranstaltungen-Tracker auch leidet viel Standardcode Rahmen im Stapel Ablaufverfolgung Segmente, obwohl es leicht von Namespaces oder Klasse Framenamen herausgefiltert werden kann. Finden Sie im Beispielcode für eine Liste der gemeinsamen Filter.

Eine weitere Einschränkung betrifft die Schleifen in asynchronen Code. Betrachten Sie den folgenden Ausschnitt:

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

Bis zum Ende der Methode würde seine Kette der Kausalität auf mehr als 30 Segmente, die immer wieder im Wechsel zwischen FirstAsync, SecondAsync und ThirdAsync Rahmen wachsen.Für eine endliche Schleife möglicherweise dadurch erträglich, obwohl es noch eine Verschwendung von doppelte Rahmen 10 Mal gespeichert ist.Jedoch kann in einigen Fällen ein Programm eine gültige Endlosschleife, z. B. im Falle einer Meldungsschleife führen.Außerdem unendliche Wiederholung eingeführt werden konnten, ohne Schleife oder Konstrukte erwarten — ein Timer, der sich für jeden Teilstrich Umplanung ist ein perfektes Beispiel.Verfolgung einer unendlichen Kausalität-Kette ist ein sicherer Weg, um Speicher, erschöpft, so dass die Menge der Daten, die gespeichert hat, irgendwie auf eine endliche Menge reduziert werden.

Den Anrufer-Infos-basierte Tracker, nicht von diesem Problem betroffen, da es einen Frame aus der Liste sofort beim Start eines Anhaltens entfernt.Es gibt zwei (kombinierbar) Ansätze, dieses für das TPL-Veranstaltungen-Szenario zu Regeln.Eine soll ältere Daten basierend auf der rollenden maximale schneiden.Das andere ist, Schleifen effizient darstellen und Doppelarbeit zu vermeiden.Für beide Ansätze empfiehlt sich auch gängige Endlosschleife Muster zu erkennen und schneiden die Kette der Kausalität explizit an diesen Punkten.

Zögern Sie nicht, finden in der begleitenden Beispielprojekt zu sehen, wie die Schleife Falten implementiert werden könnte.

Wie erwähnt, kann die TPL-Veranstaltungen-API nur Sie eine Kette der Kausalität, kein Diagramm aufzeichnen.Dies ist da die Task.WaitAll und Task.WhenAll Methoden implementiert sind, wie Uhren, wo ist die Fortsetzung geplant, nur, wenn die letzte Aufgabe in abgeschlossenen kommt und der Zähler Null erreicht.Nur die letzte abgeschlossene Aufgabe bildet damit, eine Kette der Kausalität.

Zusammenfassung

In diesem Artikel haben Sie den Unterschied zwischen eine Aufrufliste, einen return-Stack und eine Kette der Kausalität gelernt.Sie sollten jetzt Erweiterungspunkte, die das .NET Framework bereitstellt, um die Terminplanung zu verfolgen und asynchrone Operationen berücksichtigen und nutzen diese um gefangennehmen und konservieren Kausalität Ketten.Die Ansätze beschrieben Cover Kausalität in Classic und Windows Store apps, sowohl im Leben verfolgen und Post-Mortem-Debugszenarios.Sie haben auch gelernt, über das Konzept der Async-lokalen Threadspeicher und seine mögliche Implementierung für Windows Store apps.

Jetzt können Sie voran gehen und Kausalitätsverfolgung in Ihrer asynchronen Codebasis zu integrieren oder Async lokalen Threadspeicher in PARALLELRECHNUNGEN verwenden; erkunden Sie die Ereignisquellen, die das .NET Framework 4.5 und .NET für Windows Store apps anbieten, etwas neues, wie ein Tracker für Unerledigte Aufgaben in Ihrem Programm zu bauen; oder verwenden Sie diese Extension-Point Ihre eigenen Ereignisse zum Optimieren der Leistung der Anwendung ausgelöst.

Stasyuk Andriy (Andrew) ist Software Development Engineer in Test II im verwalteten Sprachen-Team bei Microsoft. Er hat sieben Jahre Erfahrung als Teilnehmer, Aufgabe Autor, Juror und Coach bei verschiedenen nationalen und internationalen Programmierung Wettbewerbe. Er arbeitete in der Finanz-Softwareentwicklung bei Paladyne/Broadridge Financial Solutions Inc. und die Deutsche Bank AG, bevor er zu Microsoft. Sein Hauptinteresse in der Programmierung sind Algorithmen, Parallelität und Denksportaufgaben.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Vance Morrison und Lucian Wischik