Comment déclarer une tâche non démarrée qui attendra une autre tâche?

9

J'ai fait ce test unitaire et je ne comprends pas pourquoi "attendre Task.Delay ()" n'attend pas!

   [TestMethod]
    public async Task SimpleTest()
    {
        bool isOK = false;
        Task myTask = new Task(async () =>
        {
            Console.WriteLine("Task.BeforeDelay");
            await Task.Delay(1000);
            Console.WriteLine("Task.AfterDelay");
            isOK = true;
            Console.WriteLine("Task.Ended");
        });
        Console.WriteLine("Main.BeforeStart");
        myTask.Start();
        Console.WriteLine("Main.AfterStart");
        await myTask;
        Console.WriteLine("Main.AfterAwait");
        Assert.IsTrue(isOK, "OK");
    }

Voici la sortie du test unitaire:

Sortie de test unitaire

Comment est-ce possible qu'un "attente" n'attende pas, et le fil principal continue?

Elo
la source
On ne sait pas trop ce que vous essayez de réaliser. Pouvez-vous s'il vous plaît ajouter votre sortie attendue?
OlegI
1
La méthode d'essai est très claire - isOK devrait être vrai
Sir Rufo
Il n'y a aucune raison de créer une tâche non démarrée. Les tâches ne sont pas des threads, elles utilisent des threads. Qu'essayez-vous de faire? Pourquoi ne pas utiliser Task.Run()après le premier Console.WriteLine?
Panagiotis Kanavos
1
@Elo, vous venez de décrire les pools de threads. Vous n'avez pas besoin d'un pool de tâches pour implémenter une file d'attente de travaux. Vous avez besoin d'une file d'attente de travaux, par exemple des objets Action <T>
Panagiotis Kanavos
2
@Elo ce que vous essayez de faire est déjà disponible dans .NET, par exemple via les classes TPL Dataflow comme ActionBlock ou les classes System.Threading.Channels plus récentes. Vous pouvez créer un ActionBlock pour recevoir et traiter des messages à l'aide d'une ou plusieurs tâches simultanées. Tous les blocs ont des tampons d'entrée avec une capacité configurable. Le DOP et la capacité vous permettent de contrôler la simultanéité, d'accélérer les demandes et d'implémenter la contre-pression - si trop de messages sont mis en file d'attente, le producteur attend
Panagiotis Kanavos

Réponses:

8

new Task(async () =>

Une tâche ne prend pas un Func<Task>, mais un Action. Il appellera votre méthode asynchrone et s'attendra à ce qu'elle se termine lorsqu'elle reviendra. Mais ce n'est pas le cas. Il renvoie une tâche. Cette tâche n'est pas attendue par la nouvelle tâche. Pour la nouvelle tâche, le travail est effectué une fois la méthode retournée.

Vous devez utiliser la tâche qui existe déjà au lieu de l'envelopper dans une nouvelle tâche:

[TestMethod]
public async Task SimpleTest()
{
    bool isOK = false;

    Func<Task> asyncMethod = async () =>
    {
        Console.WriteLine("Task.BeforeDelay");
        await Task.Delay(1000);
        Console.WriteLine("Task.AfterDelay");
        isOK = true;
        Console.WriteLine("Task.Ended");
    };

    Console.WriteLine("Main.BeforeStart");
    Task myTask = asyncMethod();

    Console.WriteLine("Main.AfterStart");

    await myTask;
    Console.WriteLine("Main.AfterAwait");
    Assert.IsTrue(isOK, "OK");
}
nvoigt
la source
4
Task.Run(async() => ... )est également une option
Sir Rufo
Vous venez de faire la même chose en tant qu'auteur de la question
OlegI
BTW lèvera myTask.Start();unInvalidOperationException
Sir Rufo
@OlegI Je ne le vois pas. Pourriez-vous l'expliquer s'il vous plaît?
nvoigt
@nvoigt Je suppose qu'il veut dire myTask.Start()produirait une exception pour son alternative et devrait être supprimé lors de l'utilisation Task.Run(...). Il n'y a aucune erreur dans votre solution.
404
3

Le problème est que vous utilisez la Taskclasse non générique , qui n'est pas destinée à produire un résultat. Ainsi, lorsque vous créez l' Taskinstance en passant un délégué asynchrone:

Task myTask = new Task(async () =>

... le délégué est traité comme async void. Un async voidn'est pas un Task, il ne peut pas être attendu, son exception ne peut pas être gérée, et c'est une source de milliers de questions posées par des programmeurs frustrés ici dans StackOverflow et ailleurs. La solution consiste à utiliser la Task<TResult>classe générique , car vous souhaitez renvoyer un résultat, et le résultat en est un autre Task. Vous devez donc créer un Task<Task>:

Task<Task> myTask = new Task<Task>(async () =>

Maintenant, lorsque vous Startl'extérieur, Task<Task>il sera terminé presque instantanément parce que son travail consiste simplement à créer l'intérieur Task. Vous devrez également attendre l'intérieur Task. Voici comment cela peut être fait:

myTask.Start();
Task myInnerTask = await myTask;
await myInnerTask;

Vous avez deux alternatives. Si vous n'avez pas besoin d'une référence explicite à l'intérieur, Taskvous pouvez simplement attendre Task<Task>deux fois l'extérieur :

await await myTask;

... ou vous pouvez utiliser la méthode d'extension intégrée Unwrapqui combine les tâches externes et internes en une seule:

await myTask.Unwrap();

Ce déballage se produit automatiquement lorsque vous utilisez la Task.Runméthode beaucoup plus populaire qui crée des tâches à chaud, de sorte que le Unwrapn'est pas utilisé très souvent de nos jours.

Si vous décidez que votre délégué asynchrone doit renvoyer un résultat, par exemple a string, vous devez déclarer la myTaskvariable comme étant de type Task<Task<string>>.

Remarque: je n'approuve pas l'utilisation de Taskconstructeurs pour créer des tâches froides. Comme une pratique est généralement mal vue, pour des raisons que je ne connais pas vraiment, mais probablement parce qu'elle est utilisée si rarement qu'elle a le potentiel de surprendre d'autres utilisateurs / mainteneurs / réviseurs ignorants du code par surprise.

Conseil général: soyez prudent chaque fois que vous fournissez un délégué asynchrone comme argument d'une méthode. Cette méthode devrait idéalement attendre un Func<Task>argument (ce qui signifie qu'il comprend les délégués asynchrones), ou au moins un Func<T>argument (ce qui signifie qu'au moins le généré Taskne sera pas ignoré). Dans le cas malheureux que cette méthode accepte un Action, votre délégué sera traité comme async void. C'est rarement ce que vous voulez, voire jamais.

Theodor Zoulias
la source
Quelle réponse technique détaillée! Merci.
Elo
@Elo mon plaisir!
Theodor Zoulias
1
 [Fact]
        public async Task SimpleTest()
        {
            bool isOK = false;
            Task myTask = new Task(() =>
            {
                Console.WriteLine("Task.BeforeDelay");
                Task.Delay(3000).Wait();
                Console.WriteLine("Task.AfterDelay");
                isOK = true;
                Console.WriteLine("Task.Ended");
            });
            Console.WriteLine("Main.BeforeStart");
            myTask.Start();
            Console.WriteLine("Main.AfterStart");
            await myTask;
            Console.WriteLine("Main.AfterAwait");
            Assert.True(isOK, "OK");
        }

entrez la description de l'image ici

BASKA
la source
3
Vous vous rendez compte que sans le await, le retard de tâche ne retardera pas réellement cette tâche, non? Vous avez supprimé la fonctionnalité.
nvoigt
Je viens de tester, @nvoigt a raison: Temps écoulé: 0: 00: 00,0106554. Et nous voyons dans votre capture d'écran: "temps écoulé: 18 ms", il devrait être> = 1000 ms
Elo
Oui, vous avez raison, je mets à jour ma réponse. Tnx. Après vos commentaires, je résous cela avec des changements minimes. :)
BASKA
1
Bonne idée d'utiliser wait () et de ne pas attendre de l'intérieur d'une tâche! Merci !
Elo