Devez-vous mettre Task.Run dans une méthode pour le rendre asynchrone?

304

J'essaie de comprendre l'async attend sous la forme la plus simple. Je veux créer une méthode très simple qui ajoute deux nombres pour cet exemple, d'accord, ce n'est pas du tout un temps de traitement, c'est juste une question de formulation d'un exemple ici.

Exemple 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Exemple 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Si j'attends DoWork1Async(), le code s'exécutera-t-il de manière synchrone ou asynchrone?

Dois-je envelopper le code de synchronisation avec Task.Runpour rendre la méthode attendable ET asynchrone afin de ne pas bloquer le thread d'interface utilisateur?

J'essaie de savoir si ma méthode est un Taskou retourne Task<T>dois-je envelopper le code avec Task.Runpour le rendre asynchrone.

Question stupide, je suis sûr, mais je vois des exemples sur le net où les gens attendent du code qui n'a rien d'asynchrone à l'intérieur et qui n'est pas enveloppé dans un Task.Runou StartNew.

Neal
la source
30
Votre premier extrait ne vous avertit-il pas?
svick

Réponses:

587

Tout d'abord, clarifions un peu la terminologie: "asynchrone" ( async) signifie qu'il peut redonner le contrôle au thread appelant avant qu'il ne démarre. Dans une asyncméthode, ces points de "rendement" sont des awaitexpressions.

Ceci est très différent du terme "asynchrone", car (mis) utilisé par la documentation MSDN pendant des années pour signifier "s'exécute sur un thread d'arrière-plan".

Pour confondre encore la question, asyncest très différent de "attendable"; il existe des asyncméthodes dont les types de retour ne sont pas attendus, et de nombreuses méthodes qui renvoient des types attendus qui ne le sont pas async.

Assez de ce qu'ils ne sont pas ; voici ce qu'ils sont :

  • Le asyncmot-clé autorise une méthode asynchrone (c'est-à-dire qu'il autorise les awaitexpressions). asyncméthodes peuvent revenir Task, Task<T>ou (si vous devez) void.
  • Tout type qui suit un certain modèle peut être attendu. Les types attendus les plus courants sont Tasket Task<T>.

Donc, si nous reformulons votre question en "comment puis-je exécuter une opération sur un thread d'arrière-plan d'une manière qui soit attendue", la réponse est d'utiliser Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Mais ce modèle est une mauvaise approche; voir ci-dessous).

Mais si votre question est "comment puis-je créer une asyncméthode qui peut renvoyer à son appelant au lieu de bloquer", la réponse est de déclarer la méthode asyncet de l'utiliser awaitpour ses points "donnant":

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Ainsi, le modèle de base des choses est de faire asyncdépendre le code des "attendables" dans ses awaitexpressions. Ces "attendables" peuvent être d'autres asyncméthodes ou simplement des méthodes régulières renvoyant des attentes. Méthodes régulières de retour Task/ Task<T> peuvent utiliser Task.Runpour exécuter du code sur un thread d'arrière - plan, ou (plus souvent) , ils peuvent utiliser TaskCompletionSource<T>ou l' un de ses raccourcis ( TaskFactory.FromAsync, Task.FromResult, etc.). Je ne recommande pas d' envelopper une méthode entière dans Task.Run; les méthodes synchrones doivent avoir des signatures synchrones, et il convient de laisser au consommateur le soin de les inclure dans un Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

J'ai une async/ awaitintro sur mon blog; à la fin, il y a de bonnes ressources de suivi. Les documents MSDN pour asyncsont également exceptionnellement bons.

Stephen Cleary
la source
8
@sgnsajgon: Oui. asyncles méthodes doivent revenir Task, Task<T>ou void. Tasket Task<T>sont attendus; voidn'est pas.
Stephen Cleary
3
En fait, une async voidsignature de méthode sera compilée, c'est juste une idée assez terrible car vous perdez votre pointeur vers votre tâche asynchrone
IEatBagels
4
@TopinFrassi: Oui, ils se compileront, mais ce voidn'est pas attendu.
Stephen Cleary
4
@ohadinho: Non, ce dont je parle dans le blog, c'est quand la méthode entière n'est qu'un appel à Task.Run(comme DoWorkAsyncdans cette réponse). Utiliser Task.Runpour appeler une méthode à partir d'un contexte d'interface utilisateur est approprié (comme DoVariousThingsFromTheUIThreadAsync).
Stephen Cleary
2
Oui, exactement. Il est valable d'utiliser Task.Runpour invoquer une méthode, mais s'il y a Task.Runtout ou presque tout le code de la méthode, alors c'est un anti-modèle - il suffit de garder cette méthode synchrone et de la déplacer Task.Runvers le haut.
Stephen Cleary
22

L'une des choses les plus importantes à retenir lors de la décoration d'une méthode avec async est qu'au moins il y a un opérateur d' attente à l'intérieur de la méthode. Dans votre exemple, je le traduirais comme indiqué ci-dessous en utilisant TaskCompletionSource .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
la source
26
Pourquoi utilisez-vous TaskCompletionSource, au lieu de simplement renvoyer la tâche retournée par la méthode Task.Run () (et de changer son corps pour retourner le résultat)?
ironique
4
Juste une petite note. Une méthode qui a une signature "async void" est généralement une mauvaise pratique et considérée comme un mauvais code car elle peut conduire à un blocage de l'interface utilisateur assez facilement. La principale exception concerne les gestionnaires d'événements asynchrones.
Jazzeroki
12

Lorsque vous utilisez Task.Run pour exécuter une méthode, Task obtient un thread de threadpool pour exécuter cette méthode. Du point de vue du thread d'interface utilisateur, il est donc "asynchrone" car il ne bloque pas le thread d'interface utilisateur. C'est très bien pour une application de bureau car vous n'avez généralement pas besoin de nombreux threads pour gérer les interactions des utilisateurs.

Cependant, pour une application Web, chaque demande est traitée par un thread de pool de threads et le nombre de requêtes actives peut donc être augmenté en enregistrant ces threads. L'utilisation fréquente de threads de pool de threads pour simuler une opération asynchrone n'est pas évolutive pour les applications Web.

True Async n'implique pas nécessairement l'utilisation d'un thread pour les opérations d'E / S, telles que l'accès aux fichiers / bases de données, etc. Vous pouvez lire ceci pour comprendre pourquoi l'opération d'E / S n'a pas besoin de threads. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

Dans votre exemple simple, il s'agit d'un calcul pur lié au processeur, donc l'utilisation de Task.Run est très bien.

zheng yu
la source
Donc, si je dois consommer une API externe synchrone dans un contrôleur d'API Web, je ne devrais PAS envelopper l'appel synchrone dans Task.Run ()? Comme vous l'avez dit, cela gardera le thread de requête initial débloqué, mais il utilise un autre thread de pool pour appeler l'api externe. En fait, je pense que c'est toujours une bonne idée, car de cette façon, il peut en théorie utiliser deux threads de pool pour traiter de nombreuses demandes, par exemple un thread peut traiter de nombreuses demandes entrantes et un autre peut appeler l'api externe pour toutes ces demandes?
stt106
Je suis d'accord, je ne dis pas que vous ne devriez pas absolument encapsuler tous les appels synchrones dans Task.Run (). Je signale simplement un problème potentiel.
zheng yu
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()c'est correct. Si vous le faites, vous changerez simplement de thread. c'est-à-dire que vous débloquez le thread de requête initial mais que vous prenez un autre thread du pool de threads qui aurait pu être utilisé pour traiter une autre requête. Le seul résultat est une surcharge de changement de contexte lorsque l'appel est terminé pour un gain absolument nul
Saeb Amini