web-dev-qa-db-fra.com

Task.WaitAll n'attend pas que la tâche soit terminée

Tout en essayant de comprendre la nouvelle programmation asynchrone en C # Task en C #, j'ai rencontré un problème qui m'a pris un peu de temps à comprendre, et je ne sais pas pourquoi.

J'ai résolu le problème, mais je ne sais toujours pas pourquoi il s'agissait d'un problème pour commencer. Je pensais simplement partager mon expérience au cas où quelqu'un se retrouverait dans la même situation.

Si des gourous souhaitent m'informer de la cause du problème, ce serait merveilleux et très apprécié. J'aime toujours savoir que pourquoi quelque chose ne marche pas!

J'ai fait une tâche de test, comme suit:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<Task<object>> testTask = Task.Factory.StartNew<Task<object>>(
    async (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            await Task.Delay((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<Task<object>>[] tasks = new Task<Task<object>>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

Et puis j'ai couru l'application pour voir ce qu'elle a craché. Voici un exemple de sortie:

6: 06: 15.5661 - Lancement de la tâche de test avec un délai de 3053 ms.
6: 06: 15.5662 - Fini l'attente.
6: 06: 18.5743 - La tâche de test est terminée après 3063,235ms.

Comme vous pouvez le constater, la déclaration Task.WaitAll(tasks); n’a pas fait grand-chose. Il a attendu un total de 1 milliseconde avant de poursuivre l'exécution.

J'ai répondu à ma propre "question" ci-dessous - mais comme je l'ai dit plus haut - si quelqu'un mieux informé que moi aimerait expliquer pourquoi cela ne fonctionne pas, veuillez le faire! (I think cela pourrait avoir quelque chose à voir avec l'exécution 'en sortie' de la méthode une fois qu'elle atteint un opérateur await - puis en revenant une fois l'attente terminée ... Mais je me trompe probablement) 

14
cjk84

Vous devriez éviter d'utiliser Task.Factory.StartNew avec async-wait. Vous devriez utiliser Task.Run à la place.

Une méthode async renvoie un Task<T>, un délégué async également. Task.Factory.StartNew renvoie également un Task<T>, où son résultat est le résultat du paramètre delegate. Ainsi, lorsqu'ils sont utilisés ensemble, ils renvoient un Task<Task<T>>>.

Tout ce que ce Task<Task<T>> fait, c'est d'exécuter le délégué jusqu'à ce qu'il y ait une tâche à retourner, c'est-à-dire lorsque la première attente est atteinte. Si vous attendez seulement que cette tâche soit terminée, vous n'attendez pas toute la méthode, mais seulement la partie avant la première attente.

Vous pouvez résoudre ce problème en utilisant Task.Unwrap qui crée un Task<T> qui représente ce Task<Task<T>>>:

Task<Task> wrapperTask = Task.Factory.StartNew(...);
Task actualTask = wrapperTask.Unwrap();
Task.WaitAll(actualTask);
19
i3arnon

Le problème avec votre code est qu'il y a deux tâches en jeu. L'un est le résultat de votre appel Task.Factory.StartNew, qui entraîne l'exécution de la fonction anonyme sur le pool de threads. Cependant, votre fonction anonyme est à son tour compilée pour produire une tâche imbriquée , représentant l'achèvement de ses opérations asynchrones. Lorsque vous attendez votre Task<Task<object>>, vous n'attendez que pour la tâche outer . Pour attendre la tâche interne, vous devez utiliser Task.Run au lieu de Task.Factory.StartNew, car elle décompresse automatiquement votre tâche interne:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<int> testTask = Task.Run(
    async () =>
    {
        DateTime startTime = DateTime.Now;
        Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), delay);
        await Task.Delay(delay);
        Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
        return delay;
    });
Task<int>[] tasks = new[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();
9
Douglas

Ici, Task.WaitAll attend la tâche externe et non la tâche interne. Utilisez Task.Run pour ne pas avoir de tâches imbriquées. C'est la solution des meilleures pratiques. Une autre solution consiste également à attendre la tâche interne. Par exemple:

Task<object> testTask = Task.Factory.StartNew(
    async (obj) =>
        {
            ...
        }
    ).Unwrap();

Ou:

testTask.Wait();
testTask.Result.Wait();
3
usr

Après avoir beaucoup bousillé et arraché les cheveux, j'ai finalement décidé de me débarrasser de l'async lambda et d'utiliser simplement la méthode System.Threading.Thread.Sleep pour voir si cela faisait une différence.

Le nouveau code a fini comme ça:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<object> testTask = Task.Factory.StartNew<object>(
    (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            System.Threading.Thread.Sleep((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<object>[] tasks = new Task<object>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

Remarque: en raison de la suppression du mot clé async de la méthode lambda, le type de tâche peut désormais être simplement Task<object> plutôt que Task<Task<object>> - vous pouvez voir cette modification dans le code ci-dessus.

Et voila! Ça a marché! J'ai le "fini d'attendre." message APRES la fin de la tâche.

Hélas, je me souviens avoir lu quelque part que vous ne devriez pas utiliser System.Threading.Thread.Sleep() dans votre code Task. Je ne me souviens plus pourquoi mais comme c'était juste pour les tests et que la plupart des tâches seraient en fait faire quelque chose qui prend du temps plutôt que prétendre faire quelque chose qui prend du temps , cela ne devrait pas vraiment être un problème.

Espérons que cela aide certaines personnes. Je ne suis certainement pas le meilleur programmeur au monde (ni même proche) et mon code n'est probablement pas génial, mais s'il aide quelqu'un, génial! :)

Merci d'avoir lu.

Edit: Les autres réponses à ma question expliquent pourquoi j'ai eu le problème que j'ai eu et cette réponse ne résout que le problème par erreur. Passer à Thread.Sleep (x) n'a eu aucun effet . Merci à toutes les personnes qui ont répondu et m'ont aidé!

0
cjk84