2014-12-20 16 views
10

Se creo una semplice classe come la seguente:Perché la parola chiave async genera un enumeratore e una struttura aggiuntiva al momento della compilazione?

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

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

ed esaminare entro NDepend, mostra che il TestMethod prendendo un bool ed essendo async Task ha una struct generato per con un enumeratore, la macchina a stati enumeratore e alcune cose aggiuntive.

enter image description here

Perché il compilatore genera una struct chiamato TestClass+<TestMethod>d__0 con un enumeratore per il metodo asincrono?

Sembra generare più IL di quanto il metodo effettivo produce. In questo esempio, il compilatore genera 35 righe di IL per la mia classe, mentre genera 81 linee di IL per la struct. Aumenta anche la complessità del codice compilato e induce NDepend a contrassegnarlo per diverse violazioni delle regole.

+2

Il codice in async/await è basato su [Pattern di enumeratore asincrono di Jeff Richter] (http://channel9.msdn.com/Blogs/Charles/Jeffrey-Richter-and-his-AsyncEnumerator). C'è già una voce che suggerisce una correzione per questo https://ndepend.uservoice.com/forums/226344-ndepend-user-voice/suggestions/6375659-exclude-compiler-generated-code-by-default. – Aron

+3

Perché è così che async/await funziona. Quello che vedi sono i [dettagli dell'implementazione] (http://msdn.microsoft.com/en-us/magazine/hh456402.aspx) della funzione. –

+0

Ciò sottolinea davvero l'importanza di assicurarsi che non si stia usando ciecamente asincrono ovunque quando non è necessario. Questo è più sovraccarico di quello che immagino mi aspettassi. –

risposta

2

Questo perché le parole chiave async e await sono solo zucchero sintattico per qualcosa chiamato coroutines.

Non ci sono istruzioni IL speciali per supportare la creazione di metodi asincroni. Invece, un metodo asincrono può essere visto come una specie di macchina di stato in qualche modo.

cercherò di fare questo esempio più breve possibile:

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

Questo è un codice asincrono come probabilmente avete abituati a: Il nostro metodo GetStringAsync in un primo momento crea un StringBuilder sincrono, allora aspetta alcuni metodi asincroni e infine restituisce il risultato. Come sarebbe implementato se non ci fosse la parola chiave await?

Aggiungere il seguente codice alla classe AsyncTest:

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

Così, ovviamente, il codice prima del primo await parola chiave rimane lo stesso. Tutto il resto viene convertito in una macchina a stati che utilizza le istruzioni goto per eseguire il codice precedente a tratti. Ogni volta che viene completata una delle attività attese, la macchina di stato avanza al passo successivo.

Questo esempio è semplificato per chiarire cosa succede dietro le quinte. Aggiungi la gestione degli errori e alcuni foreach -Letti nel tuo metodo asincrono e la macchina a stati diventa molto più complessa.

A proposito, c'è un altro costrutto in C# che fa una cosa del genere: la parola chiave yield. Ciò genera anche una macchina a stati e il codice sembra abbastanza simile a quello prodotto da await.

Per ulteriori informazioni, esaminare this CodeProject che analizza in modo più approfondito la macchina di stato generata.

+0

Grazie per la risposta ai dettagli, questo ha davvero aiutato a chiarire. Sarò sicuro di controllare anche il collegamento –

+0

In questo esempio, il metodo DoWork() è una variante semplificata di .MoveNext() nella macchina a stati asincroni, giusto? –

+0

Esatto. Ottimizzazione, cancellazione, gestione delle eccezioni, metodi di framework interni. – Frank

3

La generazione del codice originale per async era strettamente correlata a quella dei blocchi di enumerazione, quindi iniziarono a utilizzare lo stesso codice nel compilatore per quelle due trasformazioni di codice. Da allora è cambiato un bel po ', ma ha ancora alcuni ostacoli al design originale (come il nome MoveNext).

Per ulteriori informazioni sulle parti generate dal compilatore, Jon Skeet's blog series è la migliore fonte.

+0

Questa è stata una buona serie, grazie per il collegamento –

Problemi correlati