Sollte ich mir Sorgen machen, dass bei dieser asynchronen Methode die Operatoren "Warten" fehlen und synchron ausgeführt werden?

95

Ich habe eine Schnittstelle, die einige asynchrone Methoden verfügbar macht. Insbesondere sind Methoden definiert, die entweder Task oder Task <T> zurückgeben. Ich verwende die Schlüsselwörter async / await.

Ich bin gerade dabei, diese Schnittstelle zu implementieren. Bei einigen dieser Methoden hat diese Implementierung jedoch nichts zu erwarten. Aus diesem Grund erhalte ich die Compiler-Warnung "Diese asynchrone Methode verfügt nicht über 'Warten'-Operatoren und wird synchron ausgeführt ..."

Ich verstehe, warum ich den Fehler erhalte, frage mich aber, ob ich in diesem Zusammenhang etwas dagegen unternehmen soll. Es fühlt sich falsch an, Compiler-Warnungen zu ignorieren.

Ich weiß, dass ich es beheben kann, indem ich auf einen Task.Run warte, aber das fühlt sich falsch an für eine Methode, die nur wenige kostengünstige Operationen ausführt. Es hört sich auch so an, als würde es der Ausführung unnötigen Overhead hinzufügen, aber ich bin mir auch nicht sicher, ob das bereits vorhanden ist, da das Schlüsselwort async vorhanden ist.

Sollte ich die Warnungen einfach ignorieren oder gibt es eine Möglichkeit, dies zu umgehen, die ich nicht sehe?

dannykay1710
quelle
2
Es wird von den Besonderheiten abhängen. Sind Sie wirklich sicher, dass diese Vorgänge synchron ausgeführt werden sollen? Wenn Sie möchten, dass sie synchron ausgeführt werden, warum ist die Methode als gekennzeichnet async?
Servy
11
Entfernen Sie einfach das asyncSchlüsselwort. Sie können eine TaskVerwendung weiterhin zurückgeben Task.FromResult.
Michael Liu
1
@BenVoigt Google ist voll von Informationen darüber, falls das OP es noch nicht weiß.
Servy
1
@ BenVoigt Hat Michael Liu diesen Hinweis nicht schon gegeben? Verwenden Sie Task.FromResult.
1
@hvd: Das wurde später in seinem Kommentar bearbeitet.
Ben Voigt

Antworten:

147

Das asynchrone Schlüsselwort ist lediglich ein Implementierungsdetail einer Methode. Es ist nicht Teil der Methodensignatur. Wenn eine bestimmte Methodenimplementierung oder -überschreibung nichts zu erwarten hat, lassen Sie einfach das Schlüsselwort async weg und geben Sie eine abgeschlossene Aufgabe mit Task.FromResult <TResult> zurück :

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

Wenn Ihre Methode Task anstelle von Task <TResult> zurückgibt , können Sie eine abgeschlossene Task eines beliebigen Typs und Werts zurückgeben. Task.FromResult(0)scheint eine beliebte Wahl zu sein:

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

Ab .NET Framework 4.6 können Sie Task.CompletedTask auch zurückgeben :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
quelle
Vielen Dank. Ich denke, das, was mir gefehlt hat, war das Konzept, eine abgeschlossene Aufgabe zu erstellen, anstatt eine tatsächliche Aufgabe zurückzugeben, die, wie Sie sagen, mit dem asynchronen Schlüsselwort identisch wäre. Scheint jetzt offensichtlich, aber ich habe es einfach nicht gesehen!
Dannykay1710
1
Task kann für diesen Zweck mit einem statischen Element im Sinne von Task.Empty ausgeführt werden. Die Absicht wäre etwas klarer und es schmerzt mich, an all diese pflichtbewussten Aufgaben zu denken, die eine Null zurückgeben, die niemals benötigt wird.
Rupert Rawnsley
await Task.FromResult(0)? Wie wäre es await Task.Yield()?
Sushi271
1
@ Sushi271: Nein, bei einer Nicht- asyncMethode kehren Sie zurück,Task.FromResult(0) anstatt darauf zu warten.
Michael Liu
1
Eigentlich NEIN, Async ist nicht nur ein Implementierungsdetail, es gibt viele Details, die man beachten muss :). Man muss sich bewusst sein, welcher Teil synchron läuft, welcher Teil asynchron ist, was der aktuelle Synchronisationskontext ist und nur für den Datensatz: Aufgaben sind immer etwas schneller, da sich hinter Vorhängen keine Zustandsmaschine befindet :).
Ipavlu
16

Es ist durchaus vernünftig, dass einige "asynchrone" Operationen synchron abgeschlossen werden und dennoch aus Gründen des Polymorphismus dem asynchronen Aufrufmodell entsprechen.

Ein reales Beispiel hierfür sind die OS-E / A-APIs. Asynchrone und überlappende Aufrufe auf einigen Geräten werden immer inline ausgeführt (z. B. Schreiben in eine Pipe, die mithilfe des gemeinsam genutzten Speichers implementiert wurde). Sie implementieren jedoch dieselbe Schnittstelle wie mehrteilige Operationen, die im Hintergrund fortgesetzt werden.

Ben Voigt
quelle
5

Es könnte zu spät sein, aber es könnte eine nützliche Untersuchung sein:

Es geht um die innere Struktur von kompiliertem Code ( IL ):

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

es wird in 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

Und ohne Async- und Task-Methode:

 public static int GetTestData()
        {
            return 12;
        }

wird :

.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

Wie Sie den großen Unterschied zwischen diesen Methoden sehen konnten. Wenn Sie die Funktion "Warten innerhalb der asynchronen Methode" nicht verwenden und sich nicht für die Verwendung der asynchronen Methode (z. B. API-Aufruf oder Ereignishandler) interessieren, konvertiert die gute Idee diese in die normale Synchronisierungsmethode (dies spart Ihre Anwendungsleistung).

Aktualisiert:

Weitere Informationen finden Sie in den Microsoft-Dokumenten https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

Asynchrone Methoden müssen ein Schlüsselwort zum Warten haben, sonst geben sie niemals nach! Dies ist wichtig zu beachten. Wenn await im Hauptteil einer asynchronen Methode nicht verwendet wird, generiert der C # -Compiler eine Warnung, aber der Code wird kompiliert und ausgeführt, als wäre es eine normale Methode. Beachten Sie, dass dies auch unglaublich ineffizient wäre, da die vom C # -Compiler für die asynchrone Methode generierte Zustandsmaschine nichts erreichen würde.

Oleg Bondarenko
quelle
2
Darüber hinaus ist Ihre endgültige Schlussfolgerung über die Verwendung von async/awaitstark vereinfacht, da Sie sich auf Ihr unrealistisches Beispiel einer einzelnen Operation stützen, die an die CPU gebunden ist. Tasks bei ordnungsgemäßer Verwendung ermöglicht eine verbesserte Anwendungsleistung und Reaktionsfähigkeit aufgrund gleichzeitiger Aufgaben (dh paralleler Aufgaben) und eine bessere Verwaltung und Verwendung von Threads
MickyD
Das ist nur ein vereinfachtes Testbeispiel, wie ich in diesem Beitrag sagte. Ich erwähnte auch Anfragen an API- und Event-Hendler, die mit beiden Methodenversionen (asynchron und regulär) möglich waren. Auch PO sagte über die Verwendung von asynchronen Methoden, ohne im Inneren zu warten. In meinem Beitrag ging es darum, aber nicht darum, es richtig zu verwenden Tasks. Es ist eine traurige Geschichte, dass Sie nicht den gesamten Text des Beitrags lesen und schnell Schlussfolgerungen ziehen.
Oleg Bondarenko
1
Es gibt einen Unterschied zwischen einer Methode, die int(wie in Ihrem Fall) zurückgibt, und einer Methode, die zurückgibt, wie im TaskOP beschrieben. Lesen Sie seinen Beitrag und die akzeptierte Antwort erneut, anstatt die Dinge persönlich zu nehmen. Ihre Antwort ist in diesem Fall nicht hilfreich. Sie machen sich nicht einmal die Mühe, den Unterschied zwischen einer Methode zu zeigen, die darin enthalten ist awaitoder nicht. Nun hatte man das getan , dass hätte sehr gut lohnt ein upvote
MickyD
Ich denke, Sie verstehen den Unterschied zwischen asynchronen und regulären Methoden, die mit API- oder Event-Handlern aufgerufen werden, wirklich nicht. Es wurde speziell in meinem Beitrag erwähnt. Entschuldigung, dass Ihnen das wieder fehlt .
Oleg Bondarenko
4

Michael Liu hat Ihre Frage, wie Sie die Warnung vermeiden können, gut beantwortet: indem Sie Task.FromResult zurückgeben.

Ich werde den Teil "Sollte ich mir wegen der Warnung Sorgen machen" Ihrer Frage beantworten.

Die Antwort ist ja!

Der Grund dafür ist, dass die Warnung häufig auftritt, wenn Sie eine Methode aufrufen, die Taskinnerhalb einer asynchronen Methode ohne den awaitOperator zurückgegeben wird. Ich habe gerade einen Parallelitätsfehler behoben, der aufgetreten ist, weil ich eine Operation in Entity Framework aufgerufen habe, ohne auf die vorherige Operation zu warten.

Wenn Sie Ihren Code sorgfältig schreiben können, um Compiler-Warnungen zu vermeiden, fällt er bei einer Warnung wie ein schmerzender Daumen auf. Ich hätte mehrere Stunden Debuggen vermeiden können.

Vivian River
quelle
5
Diese Antwort ist einfach falsch. Hier ist der Grund: Es kann mindestens eine awaitinnerhalb der Methode an einer Stelle geben (es wird keine CS1998 geben), aber dies bedeutet nicht, dass es keinen anderen Aufruf der asnyc-Methode gibt, bei dem die Synchronisation fehlt (using awaitoder eine andere). Wenn jemand wissen möchte, wie Sie sicherstellen können, dass Sie die Synchronisation nicht versehentlich verpassen, ignorieren Sie einfach eine weitere Warnung - CS4014. Ich würde sogar empfehlen, diesen als Fehler zu bedrohen.
Victor Yarema
1

Hinweis zum Ausnahmeverhalten bei der Rückkehr Task.FromResult

Hier ist eine kleine Demo, die den Unterschied in der Ausnahmebehandlung zwischen Methoden zeigt, die markiert und nicht markiert sind 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 meiner Antwort für Wenn async Task <T> von der Schnittstelle benötigt wird, wie man eine Rückgabevariable ohne Compiler-Warnung erhält )

Tymtam
quelle