Come chiamare in modo sicuro un metodo asincrono in C # senza aspettare


322

Ho un asyncmetodo che non restituisce dati:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

Lo chiamo da un altro metodo che restituisce alcuni dati:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Chiamare MyAsyncMethod()senza attendere provoca un avviso " Poiché questa chiamata non è attesa, il metodo corrente continua a essere eseguito prima del completamento della chiamata " in Visual Studio. Nella pagina relativa a quell'avviso è indicato:

È consigliabile sopprimere l'avviso solo se si è certi di non voler attendere il completamento della chiamata asincrona e che il metodo chiamato non genererà eccezioni .

Sono sicuro di non voler attendere il completamento della chiamata; Non ho bisogno o non ho tempo di farlo. Ma la chiamata potrebbe sollevare eccezioni.

Mi sono imbattuto in questo problema alcune volte e sono sicuro che sia un problema comune che deve avere una soluzione comune.

Come posso chiamare in modo sicuro un metodo asincrono senza attendere il risultato?

Aggiornare:

Per le persone che mi suggeriscono di attendere il risultato, questo è il codice che risponde a una richiesta Web sul nostro servizio Web (API Web ASP.NET). In attesa in un contesto UI, il thread dell'interfaccia utente rimane libero, ma in attesa in una chiamata di richiesta Web attenderà il completamento dell'attività prima di rispondere alla richiesta, aumentando così i tempi di risposta senza motivo.


Perché non creare semplicemente un metodo di completamento e ignorarlo lì? Perché se è in esecuzione su thread in background. Quindi non fermerà comunque il tuo programma.
Yahya,

1
Se non si desidera attendere il risultato, l'unica opzione è ignorare / eliminare l'avviso. Se non volete attendere il risultato / esclusione quindiMyAsyncMethod().Wait()
Peter Ritchie

1
A proposito della tua modifica: non ha senso per me. Supponiamo che la risposta venga inviata al client 1 secondo dopo la richiesta e 2 secondi dopo il metodo asincrono genera un'eccezione. Cosa faresti con questa eccezione? Non è possibile inviarlo al client, se la risposta è già stata inviata. Cos'altro vorresti fare con esso?

2
@Romoku abbastanza giusto. Supponendo che qualcuno guardi il registro, comunque. :)

2
Una variante dello scenario API Web ASP.NET è un'API Web self-hosted in un processo di lunga durata (come, diciamo, un servizio Windows), in cui una richiesta crea un'attività in background lunga per fare qualcosa di costoso, ma desidera comunque ottenere rapidamente una risposta con un HTTP 202 (accettato).
David Rubin,

Risposte:


187

Se vuoi ottenere l'eccezione "in modo asincrono", puoi fare:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Ciò ti consentirà di gestire un'eccezione su un thread diverso dal thread "principale". Ciò significa che non è necessario "attendere" la chiamata MyAsyncMethod()dal thread che chiama MyAsyncMethod; ma ti consente comunque di fare qualcosa con un'eccezione, ma solo se si verifica un'eccezione.

Aggiornare:

tecnicamente, potresti fare qualcosa di simile con await:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... che sarebbe utile se avessi bisogno di usare try/ catch(o using) specificamente ma trovo ContinueWithche sia un po 'più esplicito perché devi sapere cosa ConfigureAwait(false)significa.


7
Ho trasformato questo in un metodo di estensione su Task: classe statica pubblica AsyncUtility {vuoto statico pubblico PerformAsyncTaskWithoutAwait (questa attività Task, Action <Task> exceptionHandler) {var dummy = task.ContinueWith (t => exceptionHandler (t), TaskContinuationOptions.OnlyOnFaulted); }} Utilizzo: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat ("Si è verificato un errore durante la chiamata a MyAsyncMethod: \ n {0}", t.Exception));
Mark Avenius,

2
downvoter. Commenti? Se c'è qualcosa di sbagliato nella risposta, mi piacerebbe sapere e / o risolvere.
Peter Ritchie,

2
Ehi, non ho votato in negativo, ma ... Puoi spiegare il tuo aggiornamento? La chiamata non doveva essere attesa, quindi se lo fai in quel modo, allora aspetti la chiamata, non continui sul contesto acquisito ...
Bartosz,

1
"wait" non è la descrizione corretta in questo contesto. La riga dopo la ConfiguratAwait(false)non viene eseguita fino al completamento dell'attività, ma il thread corrente non "aspetta" (cioè blocco) per quello, la riga successiva viene chiamata in modo asincrono alla chiamata in attesa. Senza ConfigureAwait(false)quella riga successiva verrebbe eseguita nel contesto della richiesta web originale. Con l' ConfigurateAwait(false)esecuzione viene eseguito nello stesso contesto del metodo asincrono (attività), liberando il contesto / thread originale per continuare ...
Peter Ritchie,

15
La versione ContinueWith non è la stessa della versione try {await} catch {}. Nella prima versione, tutto ciò che segue ContinueWith () verrà eseguito immediatamente. Il compito iniziale viene licenziato e dimenticato. Nella seconda versione, tutto ciò che segue il catch {} verrà eseguito solo dopo il completamento dell'attività iniziale. La seconda versione equivale a "wait MyAsyncMethod (). ContinueWith (t => Console.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis,

67

Dovresti prima prendere in considerazione l'idea di creare GetStringDataun asyncmetodo e di awaitrestituirlo dall'attività MyAsyncMethod.

Se sei assolutamente sicuro di non aver bisogno di gestire le eccezioni MyAsyncMethod o di sapere quando viene completato, puoi farlo:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

A proposito, questo non è un "problema comune". È molto raro voler eseguire un po 'di codice e non preoccuparsi se si completa e non preoccuparsi se si completa correttamente.

Aggiornare:

Dato che sei su ASP.NET e vuoi tornare presto, potresti trovare utile il mio post sul blog sull'argomento . Tuttavia, ASP.NET non è stato progettato per questo e non esiste alcuna garanzia che il codice verrà eseguito dopo la restituzione della risposta. ASP.NET farà del suo meglio per lasciarlo funzionare, ma non può garantirlo.

Quindi, questa è un'ottima soluzione per qualcosa di semplice come lanciare un evento in un registro in cui non importa davvero se ne perdi un po 'qua e là. Non è una buona soluzione per qualsiasi tipo di operazione critica per l'azienda. In tali situazioni, è necessario adottare un'architettura più complessa, con un modo persistente per salvare le operazioni (ad esempio, Code di Azure, MSMQ) e un processo in background separato (ad esempio Ruolo del lavoratore di Azure, Servizio Win32) per elaborarle.


2
Penso che potresti aver frainteso. Io faccio attenzione se si genera eccezioni e non riesce, ma io non voglio avere per attendere il metodo prima di restituire i miei dati. Vedi anche la mia modifica sul contesto in cui sto lavorando se questo fa la differenza.
George Powell,

5
@GeorgePowell: è molto pericoloso avere il codice in esecuzione in un contesto ASP.NET senza una richiesta attiva. Ho un post sul blog che può aiutarti , ma senza sapere di più del tuo problema non posso dire se consiglierei questo approccio o meno.
Stephen Cleary,

@StephenCleary Ho un bisogno simile. Nel mio esempio, ho / ho bisogno di un motore di elaborazione batch per funzionare nel cloud, "eseguirò il ping" del punto finale per dare il via all'elaborazione batch, ma voglio tornare immediatamente. Dal momento che il ping lo avvia, può gestire tutto da lì. Se vengono generate eccezioni, verrebbero semplicemente registrate nelle mie tabelle "BatchProcessLog / Error" ...
Ganders

8
In C # 7, puoi sostituirlo var _ = MyAsyncMethod();con _ = MyAsyncMethod();. Questo evita comunque l'avvertimento CS4014, ma rende un po 'più esplicito che non stai usando la variabile.
Brian

48

La risposta di Peter Ritchie era quella che volevo e l'articolo di Stephen Cleary sul ritorno all'inizio di ASP.NET mi è stato di grande aiuto.

Tuttavia, come problema più generale (non specifico di un contesto ASP.NET), la seguente applicazione Console mostra l'uso e il comportamento della risposta di Peter usando Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()ritorna presto senza aspettare MyAsyncMethod()e le eccezioni generate MyAsyncMethod()vengono trattate dentro OnMyAsyncMethodFailed(Task task)e non in try/ catcharoundGetStringData()


9
Rimuovi Console.ReadLine();e aggiungi un po 'di sospensione / ritardo MyAsyncMethode non vedrai mai l'eccezione.
tymtam,

17

Finisco con questa soluzione:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}


2

Immagino che sorge la domanda, perché dovresti farlo? Il motivo di asyncin C # 5.0 è che puoi aspettare un risultato. Questo metodo non è in realtà asincrono, ma semplicemente chiamato alla volta per non interferire troppo con il thread corrente.

Forse potrebbe essere meglio iniziare un thread e lasciarlo finire da solo.


2
asyncè un po 'più di un semplice "attesa" di un risultato. "await" implica che le righe che seguono "waitit" vengono eseguite in modo asincrono sullo stesso thread che ha richiamato "wait". Questo può essere fatto senza "attendere", ovviamente, ma si finisce per avere un gruppo di delegati e perdere l'aspetto sequenziale del codice (così come la capacità di usare usinge try/catch...
Peter Ritchie

1
@PeterRitchie Puoi avere un metodo funzionalmente asincrono, che non usa la awaitparola chiave e non usa la asyncparola chiave, ma non ha senso usare la asyncparola chiave senza usare anche awaitla definizione di quel metodo.
Servito il

1
@PeterRitchie Dall'affermazione: " asyncè un po 'più di un semplice" attesa "di un risultato." La asyncparola chiave (hai sottinteso che è la parola chiave racchiudendola in backtick) non significa altro che attendere il risultato. È l'asincronia, come i concetti generali di CS, che significa molto più che aspettare un risultato.
Servito il

1
@Servy asynccrea una macchina a stati che gestisce qualsiasi attesa all'interno del metodo asincrono. Se awaitnel metodo non sono presenti s, viene comunque creata quella macchina a stati, ma il metodo non è asincrono. E se il asyncmetodo ritorna void, non c'è nulla da aspettare. Quindi è di più che solo in attesa di un risultato.
Peter Ritchie,

1
@PeterRitchie Finché il metodo restituisce un'attività, puoi aspettarla. Non è necessario per la macchina a stati o la asyncparola chiave. Tutto quello che dovresti fare (e tutto ciò che accade davvero alla fine con una macchina a stati in quel caso speciale) è che il metodo viene eseguito in modo sincrono e quindi racchiuso in un'attività completata. Suppongo che tecnicamente non si rimuova solo la macchina a stati; si rimuove la macchina a stati e quindi si chiama Task.FromResult. Presumo che tu (e anche gli autori del compilatore) potresti aggiungere l'addendum da solo.
Servito il

0

Sulle tecnologie con loop di messaggi (non sono sicuro che ASP sia uno di questi), è possibile bloccare il ciclo ed elaborare i messaggi fino al termine dell'attività e utilizzare ContinueWith per sbloccare il codice:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Questo approccio è simile al blocco su ShowDialog e mantiene reattiva l'interfaccia utente.


0

Sono in ritardo alla festa qui, ma c'è una libreria fantastica che sto usando che non ho visto referenziato nelle altre risposte

https://github.com/brminnick/AsyncAwaitBestPractices

Se è necessario "Fire And Forget", si chiama il metodo di estensione sull'attività.

Passare l'azione su Exception alla chiamata ti assicura di ottenere il meglio da entrambi i mondi: non è necessario attendere l'esecuzione e rallentare gli utenti, mantenendo la capacità di gestire l'eccezione in modo elegante.

Nel tuo esempio lo useresti in questo modo:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

Offre inoltre comodi AsyncCommands che implementano ICommand, il che è ottimo per la mia soluzione MVVM Xamarin


-1

La soluzione è avviare HttpClient in un'altra attività di esecuzione senza contesto di sincronizzazione:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);

-1

Se vuoi davvero farlo. Solo per indirizzare "Chiama un metodo asincrono in C # senza attendere", puoi eseguire il metodo asincrono all'interno di a Task.Run. Questo approccio attenderà fino al MyAsyncMethodtermine.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitscartare in modo asincrono l' Resultattività, mentre l'utilizzo del solo risultato bloccherebbe fino al completamento dell'attività .


-1

Il metodo tipicamente asincrono restituisce la classe Task. Se si utilizza il Wait()metodo o la Resultproprietà e il codice genera un'eccezione, in cui viene racchiuso il tipo di eccezione, AggregateExceptionè necessario eseguire una query Exception.InnerExceptionper individuare l'eccezione corretta.

Ma è anche possibile utilizzare .GetAwaiter().GetResult()invece: attenderà anche l'attività asincrona, ma non avvolgerà l'eccezione.

Quindi, ecco un breve esempio:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Potresti anche essere in grado di restituire alcuni parametri dalla funzione asincrona, che può essere ottenuto fornendo una Action<return type>funzione asincrona extra , ad esempio in questo modo:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

Si noti che i metodi asincroni in genere hanno una ASyncdenominazione dei suffissi, solo per evitare collisioni tra le funzioni di sincronizzazione con lo stesso nome. (Ad esempio FileStream.ReadAsync) - Ho aggiornato i nomi delle funzioni per seguire questa raccomandazione.

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.