async / waitit - quando restituire un task vs vuoto?


503

In quali scenari si vorrebbe usare

public async Task AsyncMethod(int num)

invece di

public async void AsyncMethod(int num)

L'unico scenario a cui riesco a pensare è se hai bisogno dell'attività per poterne monitorare i progressi.

Inoltre, nel seguente metodo, le parole chiave asincrone e in attesa sono inutili?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
Si noti che i metodi asincroni devono sempre essere suffissi con il nome Async . L'esempio Foo()diventerebbe FooAsync().
Fred,

30
@Fred Per lo più, ma non sempre. Questa è solo la convenzione e le eccezioni accettate a questa convenzione riguardano classi basate su eventi o contratti di interfaccia, vedere MSDN . Ad esempio, non è necessario rinominare i gestori di eventi comuni, come Button1_Click.
Ben

14
Solo una nota che non dovresti usare Thread.Sleepcon i tuoi compiti che dovresti await Task.Delay(num)invece
Bob Vale,

45
@fred Non sono d'accordo, IMO che aggiunge un suffisso asincrono dovrebbe essere usato solo quando si fornisce un'interfaccia con entrambe le opzioni di sincronizzazione e asincrono. Puffo nominare le cose con asincrono quando c'è un solo intento è inutile. Il caso in questione Task.Delaynon è dato Task.AsyncDelayche tutti i metodi in questione sono Async
Non amato il

11
Stamattina ho avuto un problema interessante con un metodo controller webapi 2, che è stato async voidinvece dichiarato async Task. Il metodo si è arrestato in modo anomalo perché utilizzava un oggetto di contesto Entity Framework dichiarato come membro del controller è stato eliminato prima che il metodo terminasse l'esecuzione. Il framework disponeva il controller prima che il suo metodo finisse di essere eseguito. Ho cambiato il metodo in Task asincrono e ha funzionato.
costa,

Risposte:


417

1) Normalmente, si vorrebbe restituire a Task. L'eccezione principale dovrebbe essere quando è necessario disporre di un voidtipo di ritorno (per eventi). Se non c'è motivo di non consentire al chiamante di awaitsvolgere il proprio compito, perché non consentirlo?

2) i asyncmetodi che restituiscono voidsono speciali in un altro aspetto: rappresentano operazioni asincrone di alto livello e hanno regole aggiuntive che entrano in gioco quando l'attività restituisce un'eccezione. Il modo più semplice per mostrare la differenza è con un esempio:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fL 'eccezione è sempre "osservata". Un'eccezione che lascia un metodo asincrono di livello superiore viene semplicemente trattata come qualsiasi altra eccezione non gestita. gL 'eccezione non viene mai osservata. Quando il Garbage Collector arriva per ripulire l'attività, vede che l'attività ha provocato un'eccezione e nessuno ha gestito l'eccezione. Quando ciò accade, il TaskScheduler.UnobservedTaskExceptiongestore esegue. Non dovresti mai lasciare che ciò accada. Per usare il tuo esempio,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Sì, usa asynce awaitqui, assicurano che il tuo metodo funzioni ancora correttamente se viene generata un'eccezione.

per ulteriori informazioni, consultare: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
Intendevo finvece che gnel mio commento. L'eccezione da fviene passata a SynchronizationContext. grilascerà UnobservedTaskException, ma UTEnon blocca più il processo se non viene gestito. Ci sono alcune situazioni in cui è accettabile avere "eccezioni asincrone" come questa che vengono ignorate.
Stephen Cleary,

3
Se hai WhenAnycon più Tasks risultanti eccezioni. Spesso devi solo gestire il primo e spesso vuoi ignorare gli altri.
Stephen Cleary,

1
@StephenCleary Grazie, immagino che sia un buon esempio, anche se dipende WhenAnyin primo luogo dal motivo per cui chiami se va bene ignorare le altre eccezioni: il caso d'uso principale che ho perché finisce per aspettare le attività rimanenti al termine , con o senza eccezioni.

10
Sono un po 'confuso sul perché mi consiglia di restituire un'attività anziché annullare. Come hai detto, f () genererà e l'eccezione ma g () no. Non è meglio essere consapevoli di queste eccezioni di thread in background?
user981225,

2
@ user981225 In effetti, ma diventa quindi responsabilità del chiamante g: ogni metodo che chiama g dovrebbe essere asincrono e usare anche wait. Questa è una linea guida, non una regola difficile, puoi decidere che nel tuo programma specifico, è più facile avere g return void.

40

Ho trovato questo articolo molto utile su asynce voidscritto da Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

La linea di fondo è che si async+voidpuò bloccare il sistema e di solito dovrebbe essere usato solo sui gestori di eventi lato UI.

Il motivo alla base di ciò è il contesto di sincronizzazione utilizzato da AsyncVoidMethodBuilder, che non è presente in questo esempio. Quando non esiste un contesto di sincronizzazione ambientale, qualsiasi eccezione non gestita dal corpo di un metodo vuoto asincrono viene riproposta sul ThreadPool. Mentre apparentemente non esiste un altro luogo logico in cui si possa generare quel tipo di eccezione non gestita, lo sfortunato effetto è che il processo è stato terminato, poiché le eccezioni non gestite su ThreadPool terminano effettivamente il processo da .NET 2.0. È possibile intercettare tutte le eccezioni non gestite utilizzando l'evento AppDomain.UnhandledException, ma non è possibile ripristinare il processo da questo evento.

Quando si scrivono i gestori di eventi dell'interfaccia utente, i metodi del vuoto asincrono sono in qualche modo indolori perché le eccezioni vengono trattate allo stesso modo dei metodi non asincroni; vengono gettati sul Dispatcher. Esiste la possibilità di recuperare da tali eccezioni, con è più che corretto per la maggior parte dei casi. Al di fuori dei gestori di eventi dell'interfaccia utente, tuttavia, i metodi del vuoto asincrono sono in qualche modo pericolosi da usare e potrebbero non essere così facili da trovare.


È corretto? Ho pensato che le eccezioni all'interno dei metodi asincroni vengano catturate all'interno di Task. E anche dopo c'è sempre un sontuoso SynchronizationContext
NM

30

Ho avuto un'idea chiara da queste affermazioni.

  1. I metodi void asincroni hanno una semantica di gestione degli errori diversa. Quando viene generata un'eccezione da un'attività asincrona o da un metodo dell'attività asincrona, tale eccezione viene acquisita e posizionata sull'oggetto Task. Con i metodi void asincroni, non esiste alcun oggetto Task, quindi eventuali eccezioni generate da un metodo void asincrono verranno sollevate direttamente su SynchronizationContext (SynchronizationContext rappresenta una posizione "dove" potrebbe essere eseguito il codice) che era attiva quando il metodo vuoto asincrono iniziato

Le eccezioni da un metodo Async Void non possono essere prese con Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Queste eccezioni possono essere osservate utilizzando AppDomain.UnhandledException o un simile evento catch-all per le applicazioni GUI / ASP.NET, ma l'utilizzo di tali eventi per la gestione regolare delle eccezioni è una ricetta per la non manutenibilità (arresta in modo anomalo l'applicazione).

  1. I metodi del vuoto asincrono hanno una semantica di composizione diversa. I metodi asincroni che restituiscono Task o Task possono essere facilmente composti utilizzando wait, Task.WhenAny, Task.WhenAll e così via. I metodi asincroni che restituiscono void non forniscono un modo semplice per notificare il codice chiamante che hanno completato. È facile avviare diversi metodi di vuoto asincrono, ma non è facile determinare quando hanno finito. I metodi nulli asincroni avviseranno il loro SynchronizationContext quando iniziano e finiscono, ma un SynchronizationContext personalizzato è una soluzione complessa per il normale codice dell'applicazione.

  2. Metodo Async Void utile quando si utilizza il gestore di eventi sincrono perché aumentano le loro eccezioni direttamente su SynchronizationContext, che è simile al comportamento dei gestori di eventi sincroni

Per maggiori dettagli controlla questo link https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

Il problema con la chiamata del vuoto asincrono è che non riesci nemmeno a recuperare l'attività, non hai modo di sapere quando l'attività della funzione è stata completata (vedi https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Ecco i tre modi per chiamare una funzione asincrona:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

In tutti i casi, la funzione si trasforma in una catena di attività. La differenza è ciò che restituisce la funzione.

Nel primo caso, la funzione restituisce un'attività che alla fine produce t.

Nel secondo caso, la funzione restituisce un'attività che non ha prodotto, ma è comunque possibile attendere per sapere quando è stata completata.

Il terzo caso è quello cattivo. Il terzo caso è come il secondo caso, tranne per il fatto che non è nemmeno possibile recuperare l'attività. Non hai modo di sapere quando l'attività della funzione è stata completata.

Il caso del vuoto asincrono è un "fuoco e dimentica": inizi la catena di attività, ma non ti importa quando è finita. Quando la funzione ritorna, tutto ciò che sai è che tutto fino alla prima attesa è stato eseguito. Tutto ciò che segue la prima attesa verrà eseguito in futuro in un punto non specificato a cui non si ha accesso.


6

Penso che tu possa usare anche async voidper dare il via alle operazioni in background, purché tu stia attento a cogliere le eccezioni. Pensieri?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
Il problema con la chiamata del vuoto asincrono è che non riesci nemmeno a recuperare l'attività, non hai modo di sapere quando l'attività della funzione è stata completata
user8128167

Questo ha senso per le operazioni (rare) in background in cui non ti interessa il risultato e non vuoi che un'eccezione influisca su altre operazioni. Un caso d'uso tipico sarebbe l'invio di un evento di registro a un server di registro. Volete che ciò accada in background e non volete che il vostro gestore di servizi / richieste fallisca se il log server è inattivo. Ovviamente, devi prendere tutte le eccezioni, altrimenti il ​​tuo processo verrà interrotto. O c'è un modo migliore per raggiungere questo obiettivo?
Florian Winter,

Le eccezioni da un metodo Async Void non possono essere prese con Catch. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi

3
@SiZi il fermo è INTERNO del metodo del vuoto asincrono come mostrato nell'esempio e viene catturato.
bboyle1234,

Che dire quando i tuoi requisiti cambiano per richiedere di non fare alcun lavoro se non riesci a registrarlo - allora devi cambiare le firme in tutta la tua API, invece di cambiare semplicemente la logica che attualmente ha // Ignora Eccezione
Milney

0

La mia risposta è semplice: non puoi aspettare il metodo vuoto

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Quindi, se il metodo è asincrono, è meglio essere attendibili, perché si può perdere il vantaggio asincrono.


-2

Secondo la documentazione Microsoft , non dovrebbe MAI utilizzareasync void

Non fare ciò: L'esempio seguente usa ciò async voidche rende completa la richiesta HTTP quando viene raggiunta la prima attesa:

  • Che è SEMPRE una cattiva pratica nelle app ASP.NET Core.

  • Accede a HttpResponse al termine della richiesta HTTP.

  • Si blocca il processo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.