2015-07-22 24 views
8

Ho creato alcune app per console demo asincrone/attese e ottenere risultati strani. Codice:C# asincrono/attendono un comportamento strano nell'app console

class Program 
{ 
    public static void BeginLongIO(Action act) 
    { 
     Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(1000); 
     act(); 
     Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
    } 

    public static Int32 EndLongIO() 
    { 
     Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(500); 
     Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return 42; 
    } 

    public static Task<Int32> LongIOAsync() 
    { 
     Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var tcs = new TaskCompletionSource<Int32>(); 
     BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     }); 
     Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return tcs.Task; 
    } 

    public async static Task<Int32> DoAsync() 
    { 
     Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var res = await LongIOAsync(); 
     Thread.Sleep(1000); 
     Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return res; 
    } 

    static void Main(String[] args) 
    { 
     ticks = DateTime.Now.Ticks; 
     Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     DoAsync(); 
     Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(3000); 
     Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
    } 

    private static Int64 ticks; 
} 

Il risultato sotto:

enter image description here

Forse non capisco pienamente ciò che rende esattamente attendono. Ho pensato che se l'esecuzione arriva ad attendere, l'esecuzione ritorna al metodo del chiamante e l'attività per attendere le esecuzioni in un altro thread. Nel mio esempio tutte le operazioni vengono eseguite in un thread e l'esecuzione non ritorna al metodo del chiamante dopo aver atteso la parola chiave. Dov'è la verità?

+0

si può guardare questo: https: //channel9.msdn.com/Shows/Going+Deep/Mads-Torgersen-Inside-C-Async – Chaka

+1

'Ho pensato che se l'esecuzione viene ad attendere quindi i rendimenti di esecuzione al metodo del chiamante e al compito di attendere le esecuzioni in un altro thread. Ogni singola intro 'async' di cui sono a conoscenza (incluso [il mio] (http://blog.stephencleary.com/2012/02/async-and-await .html)) si fa in quattro per dichiarare esplicitamente che questo * non è * ciò che accade. –

risposta

2

Questo non è il modo in cui funziona async-await.

Contrassegnare un metodo come async non crea alcun thread in background. Quando si chiama un metodo async viene eseguito in modo sincrono fino a un punto asincrono e solo dopo viene restituito al chiamante.

Questo punto asincrono è quando si esegue l'operazione await non ancora completata. Quando viene completato, il resto del metodo è pianificato per essere eseguito. Questa attività dovrebbe rappresentare un'operazione asincrona effettiva (come I/O o Task.Delay).

Nel codice non c'è un punto asincrono, non c'è alcun punto in cui viene restituito il thread chiamante. Il thread diventa sempre più profondo e blocca su Thread.Sleep fino a quando questi metodi non vengono completati e restituisce DoAsync.

Prendete questo semplice esempio:

public static void Main() 
{ 
    MainAsync().Wait(); 
} 

public async Task MainAsync() 
{ 
    // calling thread 
    await Task.Delay(1000); 
    // different ThreadPool thread 
} 

Qui abbiamo un punto asincrono reale (Task.Delay) il thread chiamante ritorna Main e poi blocca sincrono sul compito. Dopo un secondo l'attività Task.Delay è completata e il resto del metodo viene eseguito su un thread diverso ThreadPool.

Se al posto di Task.Delay avremmo utilizzato Thread.Sleep, verrà eseguito sullo stesso thread di chiamata.

+0

Grazie per la rapida risposta! È utile Quindi la mia prossima domanda: come funziona attendere l'attività che ritorna dal wrapper basato su attività per i metodi che seguono al modello APM come BeginReceive e EndReceive dalla classe Socket? Questo wrapper utilizza TaskCompletionSource e restituisce anche l'attività completata. Dove ci sono un punto asincrono? – Jeksonic

+0

@Jeksonic L'utilizzo del TCS va bene. Il fatto è che i metodi Begin/End in .Net sono già asincroni. Quindi quando chiami 'Begin' inizia solo un'operazione e rilascia una discussione. Solo quando l'operazione sarà completata, chiamerà il callback fornito. – i3arnon

+0

@Jeksonic Nel tuo caso. Il metodo 'Begin' blocca solo il thread chiamante, quindi chiama il callback in modo che il thread non venga rilasciato. – i3arnon

0

la linea che in realtà funziona qualcosa su un thread in background è

Task.Run(() => { }); 

Nel tuo esempio non si sta attendendo che il compito, ma quella di un TaskCompletionSource

public static Task<int> LongIOAsync() 
    { 
     var tcs = new TaskCompletionSource<Int32>(); 

     Task.Run (() => BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     })); 

     return tcs.Task;  
    } 

Quando in attesa di LongIOAsync si è in attesa della l'attività da tcs witch viene impostata da un thread in background in un delegato assegnato a Task.Run()

applicare questa modifica:

public static Task<Int32> LongIOAsync() 
    { 
     Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var tcs = new TaskCompletionSource<Int32>(); 

     Task.Run (() => BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     })); 

     Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return tcs.Task; 
    } 

alternativa in questo caso si potrebbe di appena atteso il tornati da Task.Run(), TaskCompletionSource è per le situazioni in cui si desidera passare intorno la possibilità di impostare il vostro compito come completa o altri saggi.

0

Risposta breve è che LongIOAsync() sta bloccando.Se lo si esegue in un programma GUI, si vedrà effettivamente la GUI che si blocca brevemente, non il modo in cui async/await dovrebbe funzionare. Quindi l'intera cosa cade a pezzi.

È necessario avvolgere tutte le operazioni di lunga durata in un'attività quindi attendere direttamente su tale attività. Niente dovrebbe bloccarsi durante questo.

1

Per comprendere veramente questo comportamento, è necessario prima capire che cosa è Task e cosa async e await effettivamente fare al tuo codice.

Task è la rappresentazione CLR di "un'attività". Potrebbe essere un metodo in esecuzione su un thread pool di lavoro. Potrebbe essere un'operazione per recuperare alcuni dati da un database su una rete. La sua natura generica consente di incapsulare molte implementazioni diverse, ma fondamentalmente è necessario capire che significa solo "un'attività".

La classe Task consente di esaminare lo stato dell'attività: se è stata completata, se deve ancora essere avviata, se ha generato un errore, ecc. Questa modellazione di un'attività ci consente di comporre più facilmente i programmi che sono costruiti come una sequenza di attività, piuttosto che una sequenza di chiamate di metodo.

Considerate questo codice banale:

public void FooBar() 
{ 
    Foo(); 
    Bar(); 
} 

Ciò significa "metodo Foo esecuzione, quindi eseguire il metodo Bar Se consideriamo un'implementazione che restituisce Task da Foo e Bar, la composizione di queste chiamate è diverso. :

public void FooBar() 
{ 
    Foo().Wait(); 
    Bar().Wait(); 
} 

il significato è ora "avviare un'attività utilizzando il metodo Foo e attendere che finis h, quindi avviare un'attività utilizzando il metodo Bar e attendere che finisca. "Chiamare Wait() su un Task è raramente corretto - causa il blocco corrente fino a quando il Task non viene completato e può causare deadlock in alcuni modelli di threading comunemente utilizzati - quindi invece possiamo usare async e await per ottenere un effetto simile senza questa chiamata pericolosa.

public async Task FooBar() 
{ 
    await Foo(); 
    await Bar(); 
} 

Il async parola chiave provoca l'esecuzione del metodo per essere suddiviso in pezzi: ogni volta che si scrive await, ci vuole il codice seguente e genera una "continuazione": un metodo che deve essere eseguito come un Task dopo la l'attività attesa è completata.

Questo è diverso da Wait(), perché un Task non è collegato a un particolare modello di esecuzione. Se il numero Task restituito da Foo() rappresenta una chiamata attraverso la rete, non vi è alcun thread bloccato, in attesa del risultato: c'è un Task in attesa che l'operazione venga completata. Al termine dell'operazione, è pianificata l'esecuzione di Task - questo processo di pianificazione consente una separazione tra la definizione dell'attività e il metodo con cui viene eseguita e la potenza nell'utilizzo delle attività.

Così, il metodo può essere riassunta come:

  • avviare l'attività Foo()
  • quando questo compito completa avviare l'attività Bar
  • quando che completa attività indicano il compito metodo è completato

Nella tua app per console, non sei in attesa di y Task che rappresenta l'operazione di I/O in sospeso, motivo per cui viene visualizzato un thread bloccato: non è mai possibile creare una continuazione che verrebbe eseguita in modo asincrono.

È possibile correggere il metodo LongIOAsync per simulare il proprio I/O lungo in modo asincrono utilizzando il metodo Task.Delay(). Questo metodo restituisce un valore Task che viene completato dopo un periodo specificato. Questo ci dà l'opportunità di una continuazione asincrona.

public static async Task<Int32> LongIOAsync() 
{ 
    Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 

    await Task.Delay(1000);  

    Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
}