Jak bezpiecznie wywołać metodę asynchroniczną w języku C # bez czekania


322

Mam asyncmetodę, która nie zwraca danych:

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

Wywołuję to z innej metody, która zwraca niektóre dane:

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

Wywołanie MyAsyncMethod()bez oczekiwania powoduje pojawienie się ostrzeżenia „ Ponieważ to połączenie nie jest oczekiwane, bieżąca metoda działa jeszcze przed zakończeniem połączenia ” w Visual Studio. Na stronie tego ostrzeżenia znajduje się:

Powinieneś rozważyć wyłączenie ostrzeżenia tylko wtedy, gdy masz pewność, że nie chcesz czekać na zakończenie asynchronicznego wywołania i że wywoływana metoda nie spowoduje żadnych wyjątków .

Jestem pewien, że nie chcę czekać na zakończenie połączenia; Nie muszę ani nie mam czasu. Ale połączenie może powodować wyjątki.

Kilka razy natknąłem się na ten problem i jestem pewien, że jest to powszechny problem, który musi mieć wspólne rozwiązanie.

Jak bezpiecznie wywołać metodę asynchroniczną bez oczekiwania na wynik?

Aktualizacja:

Dla osób sugerujących, że właśnie czekam na wynik, jest to kod, który odpowiada na żądanie sieciowe w naszym serwisie internetowym (ASP.NET Web API). Oczekiwanie w kontekście interfejsu użytkownika utrzymuje wątek interfejsu użytkownika wolny, ale oczekiwanie w wywołaniu żądania WWW będzie czekać na zakończenie zadania przed odpowiedzią na żądanie, zwiększając w ten sposób czas odpowiedzi bez powodu.


Dlaczego nie po prostu stworzyć metody ukończenia i po prostu ją tam zignorować? Bo jeśli działa na wątku w tle. To i tak nie powstrzyma twojego programu przed zakończeniem.
Yahya,

1
Jeśli nie chcesz czekać na wynik, jedyną opcją jest zignorowanie / wyłączenie ostrzeżenia. Jeśli nie chcesz czekać na wynik / wyjątku następnieMyAsyncMethod().Wait()
Peter Ritchie

1
O twojej edycji: to nie ma dla mnie sensu. Powiedz, że odpowiedź jest wysyłana do klienta 1 sekundę po żądaniu, a 2 sekundy później twoja metoda asynchroniczna zgłasza wyjątek. Co zrobiłbyś z tym wyjątkiem? Nie możesz wysłać go do klienta, jeśli Twoja odpowiedź jest już wysłana. Co jeszcze byś z tym zrobił?

2
@Romoku Do przyjęcia. Przynajmniej zakładając, że ktoś patrzy na dziennik. :)

2
Odmianą w scenariuszu API sieci Web ASP.NET jest interfejs API sieci Web z własnym hostem w długotrwałym procesie (np. Usługa Windows), w którym żądanie tworzy długie zadanie w tle, aby zrobić coś kosztownego, ale nadal chce szybko uzyskaj odpowiedź za pomocą HTTP 202 (Zaakceptowano).
David Rubin

Odpowiedzi:


186

Jeśli chcesz uzyskać wyjątek „asynchronicznie”, możesz:

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

To pozwoli ci poradzić sobie z wyjątkiem w wątku innym niż wątek „główny”. Oznacza to, że nie musisz „czekać” na połączenie MyAsyncMethod()z wątku, który wywołuje MyAsyncMethod; ale nadal umożliwia zrobienie czegoś z wyjątkiem - ale tylko w przypadku wystąpienia wyjątku.

Aktualizacja:

technicznie możesz zrobić coś podobnego z await:

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

... co byłoby przydatne, gdybyś potrzebował konkretnie użyć try/ catch(lub using), ale uważam, że ContinueWithjest to nieco bardziej wyraźne, ponieważ musisz wiedzieć, co ConfigureAwait(false)oznacza.


7
Przekształciłem to w metodę rozszerzenia na Task: public static static AsyncUtility {public static void PerformAsyncTaskWithoutAwait (to zadanie Task, Action <Task> wyjmowanieHandler) {var dummy = task.ContinueWith (t => wyjmowanieHandler (t), TaskContinuationOptions.OnlyOnFaulted); }} Zastosowanie: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat („Wystąpił błąd podczas wywoływania MyAsyncMethod: \ n {0}", t.Exception));
Mark Avenius

2
downvoter. Komentarze? Jeśli w odpowiedzi jest coś złego, chciałbym wiedzieć i / lub naprawić.
Peter Ritchie

2
Hej - nie przegłosowałem, ale ... Czy możesz wyjaśnić swoją aktualizację? Nie trzeba było czekać na połączenie, więc jeśli zrobisz to w ten sposób, to zaczekasz na połączenie, po prostu nie będziesz kontynuował przechwyconego kontekstu ...
Bartosz

1
„czekaj” nie jest poprawnym opisem w tym kontekście. Wiersz po wierszu ConfiguratAwait(false)nie jest wykonywany, dopóki zadanie się nie zakończy, ale bieżący wątek nie „czeka” (tj. Blok) na to, następny wiersz jest wywoływany asynchronicznie do wywołania oczekującego. Bez ConfigureAwait(false)tego następny wiersz zostałby wykonany w oryginalnym kontekście żądania internetowego. Dzięki ConfigurateAwait(false)temu jest wykonywany w tym samym kontekście co metoda asynchroniczna (zadanie), uwalniając oryginalny kontekst / wątek, aby kontynuować ...
Peter Ritchie

15
Wersja ContinueWith to nie to samo, co wersja try {czekaj} catch {}. W pierwszej wersji wszystko po ContinueWith () zostanie wykonane natychmiast. Pierwsze zadanie zostaje zwolnione i zapomniane. W drugiej wersji wszystko po catch {} zostanie wykonane dopiero po zakończeniu początkowego zadania. Druga wersja jest odpowiednikiem „czekaj na MyAsyncMethod (). ContinueWith (t => Console.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis

67

Najpierw zastanów się nad GetStringDatastworzeniem asyncmetody i zwróć ją awaitz zadania MyAsyncMethod.

Jeśli masz absolutną pewność, że nie musisz obsługiwać wyjątków od MyAsyncMethod ani wiedzieć, kiedy się ono spełni, możesz to zrobić:

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

BTW, to nie jest „powszechny problem”. Bardzo rzadko chcemy wykonać jakiś kod i nie przejmować się, czy się on skończy i nie obchodzi, czy się to powiedzie.

Aktualizacja:

Ponieważ jesteś na ASP.NET i chcesz wrócić wcześniej, może się okazać, że mój post na blogu na ten temat jest przydatny . Jednak ASP.NET nie został do tego przeznaczony i nie ma gwarancji, że kod zostanie uruchomiony po zwróceniu odpowiedzi. ASP.NET dołoży wszelkich starań, aby pozwolić mu działać, ale nie może tego zagwarantować.

Jest to więc dobre rozwiązanie dla czegoś tak prostego, jak wrzucenie zdarzenia do dziennika, w którym tak naprawdę nie ma znaczenia, jeśli stracisz kilka tu i tam. To nie jest dobre rozwiązanie dla wszelkiego rodzaju operacji o znaczeniu krytycznym. W takich sytuacjach musisz przyjąć bardziej złożoną architekturę, z trwałym sposobem zapisywania operacji (np. Kolejki Azure, MSMQ) i osobnym procesem w tle (np. Rola pracownika Azure, usługa Win32), aby je przetworzyć.


2
Myślę, że mogłeś źle zrozumieć. I zrobić ostrożność, jeśli generuje wyjątki i nie powiedzie się, ale nie chcę, aby poczekać na metodę przed powrotem moje dane. Zobacz też moją edycję na temat kontekstu, w którym pracuję, jeśli to robi jakąkolwiek różnicę.
George Powell,

5
@GeorgePowell: Bardzo niebezpieczne jest, aby kod działał w kontekście ASP.NET bez aktywnego żądania. Mam wpis na blogu, który może ci pomóc , ale nie wiedząc więcej o twoim problemie, nie mogę powiedzieć, czy poleciłbym takie podejście, czy nie.
Stephen Cleary

@StephenCleary Mam podobną potrzebę. W moim przykładzie mam / potrzebuję silnika przetwarzania wsadowego do działania w chmurze, „pinguję” punkt końcowy, aby rozpocząć przetwarzanie wsadowe, ale chcę wrócić natychmiast. Od momentu uruchomienia polecenia ping, może on obsłużyć wszystko stamtąd. Jeśli zostaną zgłoszone wyjątki, zostaną one po prostu zalogowane w moich tabelach „BatchProcessLog / Error”
ganders

8
W języku C # 7 można zastąpić var _ = MyAsyncMethod();z _ = MyAsyncMethod();. To wciąż pozwala uniknąć ostrzeżenia CS4014, ale czyni to bardziej wyraźnym, że nie używasz zmiennej.
Brian

48

Odpowiedzi udzielił Peter Ritchie i artykuł Stephena Cleary'ego o powrocie wcześniej w ASP.NET był bardzo pomocny.

Jednak jako bardziej ogólny problem (nie specyficzny dla kontekstu ASP.NET) następująca aplikacja konsoli demonstruje użycie i zachowanie odpowiedzi Petera przy użyciu 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()wraca wcześnie bez oczekiwania, MyAsyncMethod()a zgłoszone wyjątki MyAsyncMethod()są rozpatrywane w, OnMyAsyncMethodFailed(Task task)a nie w try/ catchdookołaGetStringData()


9
Usuń Console.ReadLine();i dodaj trochę snu / opóźnienia, MyAsyncMethoda nigdy nie zobaczysz wyjątku.
tymtam

17

Kończę z tym rozwiązaniem:

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

Myślę, że powstaje pytanie, dlaczego miałbyś to zrobić? Powodem asyncw C # 5.0 jest, więc możesz poczekać na wynik. Ta metoda nie jest tak naprawdę asynchroniczna, ale po prostu wywoływana na raz, aby nie zakłócać zbytnio bieżącego wątku.

Być może lepiej jest rozpocząć wątek i pozostawić go skończeniu.


2
asyncto coś więcej niż „oczekiwanie” na wynik. „Oczekiwanie” oznacza, że ​​linie następujące po „Oczekiwanie” są wykonywane asynchronicznie w tym samym wątku, który wywołał „Oczekiwanie”. Można to oczywiście zrobić bez „czekania”, ale ostatecznie masz grupę delegatów i tracisz sekwencyjny wygląd kodu (a także możliwość używania usingi try/catch...
Peter Ritchie

1
@PeterRitchie Możesz mieć metodę, która jest funkcjonalnie asynchroniczna, nie używa awaitsłowa kluczowego i nie używa asyncsłowa kluczowego, ale nie ma sensu używanie asyncsłowa kluczowego bez uwzględnienia go awaitw definicji tej metody.
Servy

1
@PeterRitchie Z oświadczenia: „ asyncto coś więcej niż„ oczekiwanie ”na wynik”. Słowo asynckluczowe (zasugerowałeś, że jest słowem kluczowym, umieszczając je w backticks) oznacza tylko oczekiwanie na wynik. Jest to asynchronia, jako ogólne koncepcje CS, oznacza więcej niż tylko oczekiwanie na wynik.
Servy

1
@Servy asynctworzy maszynę stanu, która zarządza wszelkimi oczekiwaniami w ramach metody asynchronicznej. Jeśli awaitw metodzie nie ma żadnych znaków, nadal tworzy on maszynę stanu - ale metoda nie jest asynchroniczna. A jeśli asyncmetoda powróci void, nie ma na co czekać. To coś więcej niż tylko oczekiwanie na wynik.
Peter Ritchie

1
@PeterRitchie Tak długo, jak metoda zwróci zadanie, możesz na niego czekać. Nie ma potrzeby używania automatu stanów ani asyncsłowa kluczowego. Wszystko, co musicie zrobić (i wszystko, co naprawdę dzieje się ostatecznie z maszyną stanu w tym szczególnym przypadku), to uruchomienie metody synchronicznej, a następnie zawinięcie jej w zakończone zadanie. Przypuszczam, że technicznie nie usuwasz tylko maszyny stanu; usuwasz automat stanowy, a następnie dzwonisz Task.FromResult. Zakładałem, że ty (a także autorzy kompilatorów) możesz samodzielnie dodać ten dodatek.
Servy

0

W technologiach z pętlami komunikatów (nie jestem pewien, czy ASP jest jedną z nich), możesz zablokować pętlę i przetwarzać wiadomości aż do zakończenia zadania, a następnie użyj ContinueWith, aby odblokować kod:

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

To podejście jest podobne do blokowania w ShowDialog i nadal utrzymywania interfejsu użytkownika w gotowości.


0

Spóźniłem się na przyjęcie tutaj, ale jest niesamowita biblioteka, z której korzystałem, o której nie widziałem w innych odpowiedziach

https://github.com/brminnick/AsyncAwaitBestPractices

Jeśli potrzebujesz „Fire And Forget”, wywołujesz metodę rozszerzenia zadania.

Przekazanie akcji wyjątku do wywołania zapewnia, że ​​uzyskasz to, co najlepsze z obu światów - nie musisz czekać na wykonanie i spowolnić użytkowników, zachowując jednocześnie możliwość obsługi wyjątku w sposób wdzięczny.

W twoim przykładzie użyłbyś tego w ten sposób:

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

Daje również oczekiwane AsyncCommands wdrażające ICommand po wyjęciu z pudełka, co jest świetne dla mojego rozwiązania Xamarin MVVM


-1

Rozwiązaniem jest uruchomienie HttpClient do innego zadania wykonawczego bez kontekstu sincronization:

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

-1

Jeśli naprawdę chcesz to zrobić. Aby rozwiązać problem „Wywołaj metodę asynchroniczną w języku C # bez oczekiwania”, możesz wykonać metodę asynchroniczną w pliku a Task.Run. Takie podejście będzie czekać do MyAsyncMethodkońca.

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

awaitasynchronicznie rozpakowuje Resultzadanie, podczas gdy samo użycie wyniku blokowałoby, aż zadanie się zakończy.


-1

Zazwyczaj metoda asynchroniczna zwraca klasę zadania. Jeśli użyjesz Wait()metody lub Resultwłaściwości, a kod zgłasza wyjątek - typ wyjątku zostaje zawinięty AggregateException- musisz zapytać, Exception.InnerExceptionaby znaleźć poprawny wyjątek.

Ale można również użyć .GetAwaiter().GetResult()zamiast tego - będzie również czekać na zadanie asynchroniczne, ale nie zawinie wyjątku.

Oto krótki przykład:

public async Task MyMethodAsync()
{
}

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

Możesz także chcieć móc zwrócić jakiś parametr z funkcji asynchronicznej - można to osiągnąć poprzez dodanie dodatkowej Action<return type>funkcji asynchronicznej, na przykład:

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

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

Należy pamiętać, że metody asynchroniczne zwykle mają ASyncnazywanie sufiksami, aby uniknąć kolizji między funkcjami synchronizacji o tej samej nazwie. (Np. FileStream.ReadAsync) - Zaktualizowałem nazwy funkcji, aby zastosować się do tego zalecenia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.