Dois-je m'inquiéter de l'avertissement "Cette méthode asynchrone ne dispose pas d'opérateurs d'attente et s'exécutera de manière synchrone"

93

J'ai une interface qui expose certaines méthodes asynchrones. Plus spécifiquement, il a des méthodes définies qui retournent soit Task soit Task <T>. J'utilise les mots clés async / await.

Je suis en train d'implémenter cette interface. Cependant, dans certaines de ces méthodes, cette implémentation n'a rien à attendre. Pour cette raison, j'obtiens l'avertissement du compilateur "Cette méthode asynchrone n'a pas d'opérateurs 'await' et s'exécutera de manière synchrone ..."

Je comprends pourquoi j'obtiens l'erreur mais je me demande si je devrais faire quoi que ce soit à leur sujet dans ce contexte. Il semble erroné d'ignorer les avertissements du compilateur.

Je sais que je peux le réparer en attendant un Task.Run, mais cela ne me semble pas correct pour une méthode qui ne fait que quelques opérations peu coûteuses. Il semble également que cela ajoutera une surcharge inutile à l'exécution, mais je ne suis pas non plus sûr que cela soit déjà là car le mot-clé async est présent.

Dois-je simplement ignorer les avertissements ou existe-t-il un moyen de contourner ce problème que je ne vois pas?

dannykay1710
la source
2
Cela dépendra des détails. Voulez-vous vraiment que ces opérations soient effectuées de manière synchrone? Si vous souhaitez qu'elles soient exécutées de manière synchrone, pourquoi la méthode est-elle marquée async?
Servy le
11
Supprimez simplement le asyncmot - clé. Vous pouvez toujours renvoyer un Taskfichier using Task.FromResult.
Michael Liu
1
@BenVoigt Google regorge d'informations à ce sujet, au cas où l'OP ne le saurait déjà.
Servy le
1
@BenVoigt Michael Liu n'a-t-il pas déjà fourni cet indice? Utilisez Task.FromResult.
1
@hvd: Cela a été modifié dans son commentaire plus tard.
Ben Voigt

Réponses:

144

Le mot clé async est simplement un détail d'implémentation d'une méthode; cela ne fait pas partie de la signature de la méthode. Si une implémentation ou une substitution de méthode particulière n'a rien à attendre, omettez simplement le mot-clé async et renvoyez une tâche terminée à l'aide de Task.FromResult <TResult> :

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

Si votre méthode renvoie Task au lieu de Task <TResult> , vous pouvez renvoyer une tâche terminée de n'importe quel type et valeur. Task.FromResult(0)semble être un choix populaire:

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

Ou, à partir de .NET Framework 4.6, vous pouvez renvoyer Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
la source
Merci, je pense que le peu qui me manquait était le concept de création d'une tâche qui a été terminée, plutôt que de renvoyer une tâche réelle qui, comme vous le dites, serait la même chose que d'avoir le mot-clé async. Cela semble évident maintenant mais je ne le voyais tout simplement pas!
dannykay1710
1
Task pourrait faire avec un membre statique dans le sens de Task.Empty à cet effet. L'intention serait un peu plus claire et cela me fait mal de penser à toutes ces tâches dévouées qui renvoient un zéro qui n'est jamais nécessaire.
Rupert Rawnsley
await Task.FromResult(0)? Et pourquoi pas await Task.Yield()?
Sushi271
1
@ Sushi271: Non, dans une non- asyncméthode, vous revenez Task.FromResult(0) au lieu de l'attendre.
Michael Liu
1
En fait NON, async n'est pas seulement un détail d'implémentation, il y a beaucoup de détails autour desquels il faut être conscient :). Il faut être conscient, quelle partie fonctionne de manière synchrone, quelle partie de manière asynchrone, quel est le contexte de synchronisation actuel et juste pour mémoire, les tâches sont toujours un peu plus rapides, car il n'y a pas de machine à états derrière les rideaux :).
ipavlu
16

Il est parfaitement raisonnable que certaines opérations "asynchrones" se terminent de manière synchrone, tout en étant conformes au modèle d'appel asynchrone pour des raisons de polymorphisme.

Les API d'E / S du système d'exploitation en sont un exemple concret. Les appels asynchrones et superposés sur certains appareils se terminent toujours en ligne (écriture dans un canal implémenté à l'aide de la mémoire partagée, par exemple). Mais ils implémentent la même interface que les opérations en plusieurs parties qui continuent en arrière-plan.

Ben Voigt
la source
4

Michael Liu a bien répondu à votre question sur la manière d'éviter l'avertissement: en renvoyant Task.FromResult.

Je vais répondre à la partie «Dois-je m'inquiéter de l'avertissement» de votre question.

La réponse est oui!

La raison en est que l'avertissement se produit fréquemment lorsque vous appelez une méthode qui retourne à l' Taskintérieur d'une méthode asynchrone sans l' awaitopérateur. Je viens de corriger un bogue de concurrence survenu parce que j'ai appelé une opération dans Entity Framework sans attendre l'opération précédente.

Si vous pouvez écrire méticuleusement votre code pour éviter les avertissements du compilateur, alors quand il y a un avertissement, il ressortira comme un pouce endolori. J'aurais pu éviter plusieurs heures de débogage.

Rivière Vivian
la source
5
Cette réponse est tout simplement fausse. Voici pourquoi: il peut y avoir au moins un awaità l'intérieur de la méthode à un endroit (il n'y aura pas de CS1998) mais cela ne signifie pas qu'il n'y aura pas d'autre appel de la méthode asnyc qui manquera de synchronisation (en utilisant awaitou autre). Maintenant, si quelqu'un souhaite savoir comment s'assurer que vous ne manquez pas la synchronisation accidentellement, assurez-vous simplement de ne pas ignorer un autre avertissement - CS4014. Je recommanderais même de menacer celui-là comme une erreur.
Victor Yarema le
3

Il est peut-être trop tard, mais cela peut être une enquête utile:

Il s'agit de la structure interne du code compilé ( IL ):

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

il devient en 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

Et sans méthode asynchrone et tâche:

 public static int GetTestData()
        {
            return 12;
        }

devient :

.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

Comme vous avez pu voir la grande différence entre ces méthodes. Si vous n'utilisez pas la méthode wait inside async et que vous ne vous souciez pas de l'utilisation de la méthode async (par exemple, un appel d'API ou un gestionnaire d'événements), la bonne idée la convertira en méthode de synchronisation normale (cela économise les performances de votre application).

Mis à jour:

Il existe également des informations supplémentaires de Microsoft Docs https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

Les méthodes async doivent avoir un mot-clé await dans leur corps ou elles ne céderont jamais! Il est important de garder cela à l'esprit. Si wait n'est pas utilisé dans le corps d'une méthode async, le compilateur C # générera un avertissement, mais le code se compilera et s'exécutera comme s'il s'agissait d'une méthode normale. Notez que cela serait également incroyablement inefficace, car la machine à états générée par le compilateur C # pour la méthode async n'accomplirait rien.

Oleg Bondarenko
la source
2
De plus, votre conclusion finale sur l'utilisation de async/awaitest extrêmement simplifiée car vous la basez sur votre exemple irréaliste d'une opération unique liée au processeur. Tasks lorsqu'il est utilisé correctement, permet d'améliorer les performances et la réactivité des applications en raison de tâches simultanées (c'est-à-dire parallèles) et d'une meilleure gestion et utilisation des threads
MickyD
C'est juste un exemple de test simplifié comme je l'ai dit dans cet article. J'ai également mentionné les demandes aux api et aux hendlers d'événements où il est possible d'utiliser les deux versions des méthodes (async et régulière). PO a également parlé de l'utilisation de méthodes asynchrones sans attendre à l'intérieur. Mon message était à ce sujet mais pas sur l'utilisation correcte Tasks. C'est triste que vous ne lisiez pas tout le texte du message et que vous ne tiriez pas rapidement des conclusions.
Oleg Bondarenko
1
Il y a une différence entre une méthode qui retourne int(comme dans votre cas) et une qui retourne Taskcomme discuté par l'OP. Lire son poste et la réponse acceptée à nouveau au lieu de prendre les choses personnellement. Votre réponse n'est pas utile dans ce cas. Vous n'avez même pas la peine de montrer la différence entre une méthode qui a à l' awaitintérieur ou non. Maintenant, si vous aviez fait cela, cela aurait valu très bien un vote positif
MickyD
Je suppose que vous ne comprenez vraiment pas la différence entre la méthode asynchrone et les méthodes classiques appelées avec des api ou des gestionnaires d'événements. Cela a été spécialement mentionné dans mon message. Désolé pour vous manquez encore cela .
Oleg Bondarenko
1

Remarque sur le comportement des exceptions lors du retour Task.FromResult

Voici une petite démonstration qui montre la différence dans la gestion des exceptions entre les méthodes marquées et non marquées 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 of my answer for When async Task <T> required by interface, how to get return variable without compiler warning )

tymtam
la source