7

In realtà sto leggendo alcuni argomenti relativi alla libreria parallela Task e alla programmazione asincrona con asincrona e attesa. Il libro "C# 5.0 in a Nutshell", afferma che, quando in attesa di un'espressione utilizzando la parola chiave attendono, il compilatore trasforma il codice in qualcosa di simile a questo:Async and Await - Come viene mantenuto l'ordine di esecuzione?

var awaiter = expression.GetAwaiter(); 
awaiter.OnCompleted (() => 
{ 
var result = awaiter.GetResult(); 

Supponiamo, abbiamo questa funzione asincrona (anche dal di cui libro):

async Task DisplayPrimeCounts() 
{ 
for (int i = 0; i < 10; i++) 
Console.WriteLine (await GetPrimesCountAsync (i*1000000 + 2, 1000000) + 
" primes between " + (i*1000000) + " and " + ((i+1)*1000000-1)); 
Console.WriteLine ("Done!"); 
} 

la chiamata del metodo 'GetPrimesCountAsync' saranno accodati ed eseguito su un thread pool. In generale, invocare più thread all'interno di un ciclo for ha il potenziale per introdurre condizioni di gara.

Quindi, come fa il CLR a garantire che le richieste vengano elaborate nell'ordine in cui sono state elaborate? Dubito che il compilatore converta semplicemente il codice nel modo sopra descritto, poiché ciò disaccoppierà il metodo 'GetPrimesCountAsync' dal ciclo for.

+1

Crea una macchina a stati. Inoltre, il codice non verrà eseguito in parallelo, ma semplicemente libererà il thread mentre attende che ogni chiamata a 'GetPrimesCountAsync' venga completata in ordine. – juharr

risposta

14

Solo per il gusto della semplicità, io sto andando a sostituire il vostro esempio con uno che è un po 'più semplice, ma ha tutte le stesse proprietà significative:

async Task DisplayPrimeCounts() 
{ 
    for (int i = 0; i < 10; i++) 
    { 
     var value = await SomeExpensiveComputation(i); 
     Console.WriteLine(value); 
    } 
    Console.WriteLine("Done!"); 
} 

L'ordinamento viene mantenuto a causa della definizione del tuo codice. Immaginiamo di superarlo.

  1. Questo metodo è chiamato primo
  2. La prima riga di codice è il ciclo for, così i è inizializzato.
  3. Il controllo del ciclo passa, quindi andiamo al corpo del ciclo.
  4. SomeExpensiveComputation viene chiamato. Dovrebbe restituire uno Task<T> molto rapidamente, ma il lavoro che farebbe continuerà ad andare avanti in background.
  5. Il resto del metodo viene aggiunto come una continuazione dell'attività restituita; continuerà l'esecuzione al termine di tale attività.
  6. Dopo che l'attività è stata restituita da SomeExpensiveComputation, il risultato viene archiviato in value.
  7. value viene stampato sulla console.
  8. GOTO 3; si noti che l'operazione costosa esistente è già terminata prima di passare al punto 4 per la seconda volta e avviare il successivo.

Per quanto riguarda il modo in cui il compilatore C# esegue effettivamente il passaggio 5, lo fa creando una macchina a stati. Fondamentalmente ogni volta che c'è un await c'è un'etichetta che indica dove è stato interrotto, e all'inizio del metodo (o dopo che è stato ripreso dopo gli eventuali fuochi di continuazione) controlla lo stato corrente e fa un goto nel punto in cui era stato interrotto. . Ha anche bisogno di issare tutte le variabili locali nei campi di una nuova classe in modo da mantenere lo stato di quelle variabili locali.

Ora questa trasformazione non è effettivamente eseguita nel codice C#, è fatta in IL, ma è una sorta di morale equivalente al codice che ho mostrato sopra in una macchina a stati. Si noti che questo non è valido C# (non è possibile goto in un ciclo for come questo, ma tale restrizione non si applica al codice IL effettivamente utilizzato.Ci sono anche andando essere differenze tra questo e quello che # C in realtà lo fa, ma è dovrebbe darvi un'idea di base di quello che sta succedendo qui:

internal class Foo 
{ 
    public int i; 
    public long value; 
    private int state = 0; 
    private Task<int> task; 
    int result0; 
    public Task Bar() 
    { 
     var tcs = new TaskCompletionSource<object>(); 
     Action continuation = null; 
     continuation =() => 
     { 
      try 
      { 
       if (state == 1) 
       { 
        goto state1; 
       } 
       for (i = 0; i < 10; i++) 
       { 
        Task<int> task = SomeExpensiveComputation(i); 
        var awaiter = task.GetAwaiter(); 
        if (!awaiter.IsCompleted) 
        { 
         awaiter.OnCompleted(() => 
         { 
          result0 = awaiter.GetResult(); 
          continuation(); 
         }); 
         state = 1; 
         return; 
        } 
        else 
        { 
         result0 = awaiter.GetResult(); 
        } 
       state1: 
        Console.WriteLine(value); 
       } 
       Console.WriteLine("Done!"); 
       tcs.SetResult(true); 
      } 
      catch (Exception e) 
      { 
       tcs.SetException(e); 
      } 
     }; 
     continuation(); 
    } 
} 

Nota che ho ignorato cancellazione compito per il bene di questo esempio , Ho ignorato l'intero concetto di catturare il contesto di sincronizzazione corrente, c'è un po 'più in corso con la gestione degli errori, ecc. Non considerarlo un'implementazione completa.

+0

Grazie per la risposta. A quanto ho capito, il ciclo for viene elaborato molto rapidamente, creando così 10 attendenti (compiti) da attendere. L'unica continuazione qui è la chiamata a 'Console.WriteLine'. In che modo il CLR garantisce che il ciclo for verrà continuato solo quando l'attività è stata completata? –

+0

Perché è così che funziona il metodo 'OnCompleted' del cameriere. Viene attivato solo quando l'operazione che rappresenta completa. In questo caso, il waiter è il 'Task' awaiter, in modo efficace è solo l'implementazione della classe' Task' che chiama il metodo dopo che il 'Task' che rappresenta è effettivamente fatto, e non prima di allora. Non ha nulla a che fare con il CLR. – Servy

+0

Penso di averlo capito ora. L'intero ciclo for (così come il suo stato) è parte della continuazione. Pertanto, il ciclo for non verrà proseguito fino a quando ogni singolo utente non avrà completato la procedura. Questo è esattamente come pensavo, perché solo in questo modo lo stato del ciclo for è incorporato nella continuazione, consentendo così l'esecuzione delle attività nell'ordine corretto. –

7

La chiamata del metodo 'GetPrimesCountAsync' verrà accodata ed eseguita su un pool di thread.

No. await non avvia alcun tipo di elaborazione in background. Attende il completamento dell'elaborazione esistente. Spetta a GetPrimesCountAsync farlo (ad esempio utilizzando Task.Run). È più chiaro in questo modo:

var myRunningTask = GetPrimesCountAsync(); 
await myRunningTask; 

Il ciclo continua solo quando l'attività attesa è stata completata. Non c'è mai più di un compito in sospeso.

Quindi come fa il CLR a garantire che le richieste vengano elaborate nell'ordine in cui sono state apportate?

Il CLR non è coinvolto.

Dubito che il compilatore converta semplicemente il codice nel modo sopra descritto, poiché ciò disaccoppierà il metodo 'GetPrimesCountAsync' dal ciclo for.

la trasformazione che si dimostra è fondamentalmente proprio a meno di notare che la prossima iterazione del ciclo non viene avviato subito ma nel callback. Questo è ciò che serializza l'esecuzione.

+0

Grazie. Questa è anche una risposta molto utile. Hai ragione: la parola chiave await si aspetta solo un oggetto che implementa "INotifyCompletion" per il completamento. Spetta all'implementazione di 'GetResult' eseguirlo su un thread pool di thread. –