Czy powinienem się martwić ostrzeżeniem „Ta metoda asynchroniczna nie ma operatorów„ czekaj ”i będzie działać synchronicznie”

93

Mam interfejs, który udostępnia niektóre metody asynchroniczne. Dokładniej, zdefiniowano metody, które zwracają Task lub Task <T>. Używam słów kluczowych async / await.

Jestem w trakcie wdrażania tego interfejsu. Jednak w przypadku niektórych z tych metod implementacja ta nie ma na co czekać. Z tego powodu otrzymuję ostrzeżenie kompilatora „Ta metoda asynchroniczna nie ma operatorów„ await ”i będzie działać synchronicznie ...”

Rozumiem, dlaczego otrzymuję błąd, ale zastanawiam się, czy powinienem coś z nimi zrobić w tym kontekście. Ignorowanie ostrzeżeń kompilatora wydaje się niewłaściwe.

Wiem, że mogę to naprawić, czekając na Task.Run, ale wydaje się to niewłaściwe w przypadku metody, która wykonuje tylko kilka niedrogich operacji. Wygląda również na to, że doda niepotrzebne obciążenie do wykonania, ale nie jestem również pewien, czy to już istnieje, ponieważ słowo kluczowe async jest obecne.

Powinienem po prostu zignorować ostrzeżenia, czy jest sposób na obejście tego, którego nie widzę?

dannykay1710
źródło
2
Zależy to od szczegółów. Czy na pewno chcesz, aby te operacje były wykonywane synchronicznie? Jeśli chcesz, aby były wykonywane synchronicznie, dlaczego metoda jest oznaczona jako async?
Servy
11
Po prostu usuń asyncsłowo kluczowe. Nadal możesz zwrócić plik Taskusing Task.FromResult.
Michael Liu
1
@BenVoigt Google jest pełen informacji na ten temat, na wypadek, gdyby OP jeszcze nie wiedział.
Servy
1
@BenVoigt Czy Michael Liu nie udzielił już tej wskazówki? Użyj Task.FromResult.
1
@hvd: To zostało później zmienione w jego komentarzu.
Ben Voigt

Odpowiedzi:

144

Asynchroniczny kluczowe jest jedynie szczegół wdrożenie metody; nie jest częścią sygnatury metody. Jeśli jedna implementacja lub przesłonięcie metody nie ma nic do czekania, po prostu pomiń słowo kluczowe async i zwróć ukończone zadanie za pomocą Task.FromResult <TResult> :

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

Jeśli Twoja metoda zwraca Task zamiast Task <TResult> , możesz zwrócić ukończone zadanie dowolnego typu i wartości. Task.FromResult(0)wydaje się być popularnym wyborem:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

Lub, od .NET Framework 4.6, możesz zwrócić Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
źródło
Dzięki. Myślę, że brakowało mi koncepcji stworzenia zadania, które zostało ukończone, zamiast zwracania rzeczywistego zadania, które, jak powiedziałeś, byłoby tym samym, co posiadanie słowa kluczowego async. Wydaje się to teraz oczywiste, ale po prostu tego nie widziałem!
dannykay1710
1
W tym celu Task może mieć statyczny element członkowski na wzór Task.Empty. Zamiar byłby nieco jaśniejszy i boli mnie myślenie o tych wszystkich obowiązkowych Zadaniach, które zwracają zero, które nigdy nie jest potrzebne.
Rupert Rawnsley
await Task.FromResult(0)? A co powiesz await Task.Yield()?
Sushi271,
1
@ Sushi271: Nie, w nie asyncsposób, to powrót Task.FromResult(0) zamiast oczekując go.
Michael Liu
1
Właściwie NIE, async to nie tylko szczegół implementacji, jest wiele szczegółów, o których trzeba wiedzieć :). Trzeba mieć świadomość, która część działa synchronicznie, a która asynchronicznie, jaki jest aktualny kontekst synchronizacji i tylko dla rekordu. Zadania są zawsze trochę szybsze, bo za zasłonami nie ma automatu stanu :).
ipavlu,
16

Jest całkowicie uzasadnione, że niektóre operacje „asynchroniczne” kończą się synchronicznie, ale nadal są zgodne z modelem wywołań asynchronicznych ze względu na polimorfizm.

Przykładem tego w rzeczywistości są interfejsy API we / wy systemu operacyjnego. Asynchroniczne i nakładające się wywołania na niektórych urządzeniach zawsze kończą się w tekście (na przykład zapisywanie do potoku zaimplementowanego przy użyciu pamięci współdzielonej). Ale implementują ten sam interfejs, co operacje wieloczęściowe, które są kontynuowane w tle.

Ben Voigt
źródło
4

Michael Liu dobrze odpowiedział na Twoje pytanie, jak uniknąć ostrzeżenia: zwracając Task.FromResult.

Odpowiem na część Twojego pytania „Czy powinienem się martwić o ostrzeżenie”.

Odpowiedź brzmi tak!

Przyczyną tego jest to, że ostrzeżenie często pojawia się, gdy wywołujesz metodę, która zwraca Taskwewnątrz metody asynchronicznej bez rozszerzeniaawait operatora. Właśnie naprawiłem błąd współbieżności, który wystąpił, ponieważ wywołałem operację w Entity Framework bez oczekiwania na poprzednią operację.

Jeśli potrafisz skrupulatnie napisać kod, aby uniknąć ostrzeżeń kompilatora, to gdy pojawi się ostrzeżenie, będzie ono wyróżniać się jak bolący kciuk. Mogłem uniknąć kilku godzin debugowania.

Rzeka Vivian
źródło
5
Ta odpowiedź jest po prostu błędna. Oto dlaczego: co najmniej jeden awaitwewnątrz metody może znajdować się w jednym miejscu (nie będzie CS1998), ale nie oznacza to, że nie będzie innego wywołania metody asnyc, która nie będzie miała synchronizacji (using awaitlub jakikolwiek inny). Teraz, jeśli ktoś chciałby wiedzieć, jak upewnić się, że przypadkowo nie przegapisz synchronizacji, po prostu nie zignoruj ​​kolejnego ostrzeżenia - CS4014. Poleciłbym nawet zagrozić temu jako błąd.
Victor Yarema
3

Może być za późno, ale może być przydatne dochodzenie:

Chodzi o wewnętrzną strukturę skompilowanego kodu ( IL ):

 public static async Task<int> GetTestData()
    {
        return 12;
    }

staje się w IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

I bez metody asynchronicznej i zadaniowej:

 public static int GetTestData()
        {
            return 12;
        }

staje się :

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

Jak widać, duża różnica między tymi metodami. Jeśli nie używasz await w metodzie async i nie zależy Ci na używaniu metody async (na przykład wywołanie API lub program obsługi zdarzeń), dobrym pomysłem jest przekonwertowanie jej na normalną metodę synchronizacji (oszczędza to wydajność aplikacji).

Zaktualizowano:

Istnieją również dodatkowe informacje z Microsoft Docs https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

metody async muszą mieć słowo kluczowe await w swoim ciele, w przeciwnym razie nigdy się nie uda! Należy o tym pamiętać. Jeśli await nie jest używany w treści metody asynchronicznej, kompilator C # wygeneruje ostrzeżenie, ale kod zostanie skompilowany i uruchomiony tak, jakby był zwykłą metodą. Zauważ, że byłoby to również niewiarygodnie nieefektywne, ponieważ maszyna stanu wygenerowana przez kompilator C # dla metody asynchronicznej niczego by nie osiągnęła.

Oleg Bondarenko
źródło
2
Dodatkowo twój ostateczny wniosek dotyczący użycia programu async/awaitjest znacznie uproszczony, ponieważ opierasz go na nierealistycznym przykładzie pojedynczej operacji związanej z procesorem. TaskJeśli jest używany prawidłowo, pozwala na lepszą wydajność aplikacji i responsywność dzięki współbieżnym zadaniom (tj. równoległym) oraz lepszemu zarządzaniu i wykorzystaniu wątków
MickyD
To tylko test uproszczonego przykładu, jak powiedziałem w tym poście. Wspomniałem również o żądaniach do api i hendlerach zdarzeń, w przypadku których możliwe jest użycie obu wersji metod (async i regular). Również PO powiedział o używaniu metod asynchronicznych bez czekania w środku. Mój post był o tym, ale nie o prawidłowym używaniu Tasks. To smutna historia, że ​​nie czytasz całego tekstu postu i nie wyciągasz szybko wniosków.
Oleg Bondarenko
1
Istnieje różnica między metodą, która zwraca int(jak w twoim przypadku), a metodą zwracającą, Tasktaką jak omówiona przez OP. Przeczytaj swój post i zaakceptowanej odpowiedź znowu zamiast podejmowania rzeczy osobiście. Twoja odpowiedź nie jest pomocna w tym przypadku. Nawet nie zadajesz sobie trudu, aby pokazać różnicę między metodą, która ma awaitwnętrze lub nie. Gdybyś to zrobił, byłby to bardzo dobry wybór
MickyD
Wydaje mi się, że naprawdę nie rozumiesz różnicy między metodą asynchroniczną a zwykłymi metodami wywoływanymi za pomocą interfejsu API lub obsługi zdarzeń. Zostało to specjalnie wspomniane w moim poście. Przepraszam, że znowu tego brakuje .
Oleg Bondarenko
1

Zwróć uwagę na zachowanie wyjątków podczas powrotu Task.FromResult

Oto małe demo, które pokazuje różnicę w obsłudze wyjątków między metodami oznaczonymi i nieoznaczonymi async.

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(Cross post mojej odpowiedzi dla When async Task <T> wymagane przez interfejs, jak uzyskać zmienną zwracaną bez ostrzeżenia kompilatora )

tymtam
źródło