2014-12-20 3 views
10

Wenn ich eine einfache Klasse wie folgt zu erstellen:Warum generiert das Schlüsselwort async beim Kompilieren einen Enumerator und eine zusätzliche Struktur?

public class TestClass 
{ 
    public Task TestMethod(int someParameter) 
    { 
     return Task.FromResult(someParameter); 
    } 

    public async Task TestMethod(bool someParameter) 
    { 
     await Task.FromResult(someParameter); 
    } 
} 

und untersuchen es innerhalb NDepend, zeigt es, dass die TestMethod einen Bool nehmen und async Task zu sein hat eine Struktur für sie mit einem Enumerator erzeugt, der Enumerator Zustandsmaschine und ein paar zusätzliche Sachen.

enter image description here

Warum der Compiler eine Struktur erzeugen genannt TestClass+<TestMethod>d__0 mit einem Enumerator für die Asynchron-Methode?

Es scheint mehr IL generieren als was die eigentliche Methode produziert. In diesem Beispiel generiert der Compiler 35 IL-Zeilen für meine Klasse, während er für die Struktur 81 IL-Zeilen generiert. Es erhöht auch die Komplexität des kompilierten Codes und verursacht, dass NDepend es für mehrere Regelverletzungen kennzeichnet.

+2

Der Code in async/await basiert auf [Jeff Richters Async Enumerator-Muster] (http://channel9.msdn.com/Blogs/Charles/Jeffrey-Richter-and-his-AsyncEnumerator). Es gibt bereits eine Stimme, die einen Fix für dieses https://ndepend.uservoice.com/forums/226364-ndepend-user-voice/suggestions/6375659-exclude-compiler-generated-code-by-default vorschlägt. – Aron

+3

Denn so funktioniert async/wait. Was Sie sehen, ist das [Implementierungsdetail] (http://msdn.microsoft.com/en-us/magazine/hh456402.aspx) des Features. –

+0

Dies unterstreicht wirklich die Wichtigkeit, sicherzustellen, dass Sie nicht überall blind async verwenden, wenn es nicht benötigt wird. Das ist mehr Aufwand als das, was ich erwartet habe. –

Antwort

2

Dies ist, weil die async und await Schlüsselwörter sind nur syntaktische Zucker für etwas namens coroutines.

Es gibt keine speziellen IL-Anweisungen zur Unterstützung der Erstellung asynchroner Methoden. Stattdessen kann eine asynchrone Methode irgendwie als eine Art Zustandsmaschine angesehen werden.

Ich werde versuchen, dieses Beispiel so kurz wie möglich zu machen:

[TestClass] 
public class AsyncTest 
{ 
    [TestMethod] 
    public async Task RunTest_1() 
    { 
     var result = await GetStringAsync(); 
     Console.WriteLine(result); 
    } 

    private async Task AppendLineAsync(StringBuilder builder, string text) 
    { 
     await Task.Delay(1000); 
     builder.AppendLine(text); 
    } 

    public async Task<string> GetStringAsync() 
    { 
     // Code before first await 
     var builder = new StringBuilder(); 
     var secondLine = "Second Line"; 

     // First await 
     await AppendLineAsync(builder, "First Line"); 

     // Inner synchronous code 
     builder.AppendLine(secondLine); 

     // Second await 
     await AppendLineAsync(builder, "Third Line"); 

     // Return 
     return builder.ToString(); 
    } 
} 

Dieses einig Asynchron-Code ist wie Sie wahrscheinlich gewöhnt haben: Unsere GetStringAsync Methode zunächst erzeugen ein StringBuilder synchron, dann wartet es einige asynchrone Methoden und schließlich gibt es das Ergebnis zurück. Wie würde dies implementiert werden, wenn es kein await Schlüsselwort gibt?

Fügen Sie den folgenden Code in die AsyncTest Klasse:

[TestMethod] 
public async Task RunTest_2() 
{ 
    var result = await GetStringAsyncWithoutAwait(); 
    Console.WriteLine(result); 
} 

public Task<string> GetStringAsyncWithoutAwait() 
{ 
    // Code before first await 
    var builder = new StringBuilder(); 
    var secondLine = "Second Line"; 

    return new StateMachine(this, builder, secondLine).CreateTask(); 
} 

private class StateMachine 
{ 
    private readonly AsyncTest instance; 
    private readonly StringBuilder builder; 
    private readonly string secondLine; 
    private readonly TaskCompletionSource<string> completionSource; 

    private int state = 0; 

    public StateMachine(AsyncTest instance, StringBuilder builder, string secondLine) 
    { 
     this.instance = instance; 
     this.builder = builder; 
     this.secondLine = secondLine; 
     this.completionSource = new TaskCompletionSource<string>(); 
    } 

    public Task<string> CreateTask() 
    { 
     DoWork(); 
     return this.completionSource.Task; 
    } 

    private void DoWork() 
    { 
     switch (this.state) 
     { 
      case 0: 
       goto state_0; 
      case 1: 
       goto state_1; 
      case 2: 
       goto state_2; 
     } 

     state_0: 
      this.state = 1; 

      // First await 
      var firstAwaiter = this.instance.AppendLineAsync(builder, "First Line") 
             .GetAwaiter(); 
      firstAwaiter.OnCompleted(DoWork); 
      return; 

     state_1: 
      this.state = 2; 

      // Inner synchronous code 
      this.builder.AppendLine(this.secondLine); 

      // Second await 
      var secondAwaiter = this.instance.AppendLineAsync(builder, "Third Line") 
              .GetAwaiter(); 
      secondAwaiter.OnCompleted(DoWork); 
      return; 

     state_2: 
      // Return 
      var result = this.builder.ToString(); 
      this.completionSource.SetResult(result); 
    } 
} 

So offensichtlich der Code vor dem ersten await Schlüsselwort bleibt genau das gleiche. Alles andere wird in eine Zustandsmaschine umgewandelt, die goto Anweisungen verwendet, um Ihren vorherigen Code stückweise auszuführen. Jedes Mal, wenn eine der erwarteten Aufgaben abgeschlossen ist, geht die Zustandsmaschine zum nächsten Schritt über.

Dieses Beispiel ist zu vereinfacht, um zu verdeutlichen, was hinter den Kulissen geschieht. Fügen Sie Fehlerbehandlung und einige foreach -Loops in Ihrer asynchronen Methode hinzu, und die Zustandsmaschine wird viel komplexer.

Übrigens gibt es ein anderes Konstrukt in C#, das so etwas tut: das Schlüsselwort yield. Dies erzeugt auch eine Zustandsmaschine und der Code sieht sehr ähnlich aus, was await erzeugt.

Weitere Informationen finden Sie unter this CodeProject, die einen tieferen Einblick in die generierte Zustandsmaschine gibt.

+0

Danke für die Details beantworten, das hat wirklich geholfen, es klar zu machen. Ich werde sicher sein, den Link auch zu überprüfen –

+0

In diesem Beispiel ist Ihre DoWork() -Methode eine vereinfachte Variante von .MoveNext() in der Async-Statusmaschine richtig? –

+0

Das stimmt. Minus-Optimierung, Abbruch, Ausnahmebehandlung, interne Framework-Methoden. – Frank

3

Die ursprüngliche Codegenerierung für async war eng mit der von Enumeratorblöcken verwandt, so dass sie mit dem gleichen Code im Compiler für diese beiden Codetransformationen begonnen haben. Es hat sich seitdem ziemlich verändert, aber es hat immer noch einige Überbleibsel vom ursprünglichen Design (wie der Name MoveNext).

Für mehr über die Compiler-generierten Teile ist Jon Skeet's blog series die beste Quelle.

+0

Das war eine gute Serie, danke für den Link –

Verwandte Themen