Pourquoi l’appel est-il en attente de terminer la tâche parent prématurément?

J’essaie de créer un contrôle qui expose un événement DoLoading auquel les consommateurs peuvent s’abonner pour effectuer des opérations de chargement. Pour des raisons pratiques, les gestionnaires d’événements doivent être appelés à partir du thread d’interface utilisateur, ce qui permet aux consommateurs de mettre à jour l’interface utilisateur à volonté. Ils pourront également utiliser async / wait pour effectuer des tâches de longue durée sans bloquer le thread d’interface utilisateur.

Pour cela, j’ai déclaré le délégué suivant:

 public delegate Task AsyncEventHandler(object sender, TEventArgs e); 

Cela permet aux consommateurs de s’inscrire à l’événement:

 public event AsyncEventHandler DoLoading; 

L’idée est que les consommateurs s’abonnent à l’événement de la sorte (cette ligne est exécutée dans le fil de l’interface utilisateur):

 loader.DoLoading += async (s, e) => { for (var i = 5; i > 0; i--) { loader.Text = i.ToSsortingng(); // UI update await Task.Delay(1000); // long-running task doesn't block UI } }; 

À un moment opportun, TaskScheduler un TaskScheduler pour le thread d’interface utilisateur et le stocke dans _uiScheduler .

L’événement est déclenché le cas échéant par le loader avec la ligne suivante (cela se produit dans un thread aléatoire):

 this.PerformLoadingActionAsync().ContinueWith( _ => { // Other operations that must happen on UI thread }, _uiScheduler); 

Notez que cette ligne n’est pas appelée à partir du thread d’interface utilisateur, mais doit mettre à jour l’interface utilisateur lorsque le chargement est terminé. J’utilise donc ContinueWith pour exécuter le code sur le planificateur de tâches d’interface utilisateur lorsque la tâche de chargement est terminée.

J’ai essayé plusieurs variantes des méthodes suivantes, dont aucune n’a fonctionné, alors voici où je suis:

 private async Task PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread and await its execution Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState)); // This can be ignored for now as it completes immediately Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(evenHandlerTask, commandTask); } private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { await handler(this, mustLoadPreviousRunningState); } } 

Comme vous pouvez le constater, je commence deux tâches et j’attends mon ContinueWith d’avant pour en exécuter une complète.

La commandTask termine immédiatement et peut donc être ignorée pour le moment. Selon eventHandlerTask , la eventHandlerTask ne doit être complétée que par le gestionnaire d’événements terminé, car j’attends l’appel de la méthode qui appelle le gestionnaire d’événements et le gestionnaire d’événements lui-même.

Cependant, ce qui se passe réellement, c’est que les tâches sont terminées dès que la ligne await Task.Delay(1000) dans mon gestionnaire d’événements est exécuté.

Pourquoi est-ce et comment puis-je obtenir le comportement que j’attends?

Vous avez correctement compris que StartNew() renvoie la Task dans ce cas, et vous vous souciez de la Task interne (bien que je ne sois pas sûr de la raison de l’attente de la Task externe avant de lancer commandTask ).

Mais ensuite, vous retournez Task et ignorez la Task interne. Ce que vous devez faire est d’utiliser await au lieu de return et de changer le type de retour de PerformLoadingActionAsync() en Task seulement:

 await Task.WhenAll(evenHandlerTask, commandTask); 

Quelques notes supplémentaires:

  1. Utiliser des gestionnaires d’événements de cette manière est assez dangereux, car vous vous souciez de la Task renvoyée par le gestionnaire, mais s’il y a plus de gestionnaires, seule la dernière Task sera renvoyée si vous déclenchez l’événement normalement. Si vous voulez vraiment faire cela, vous devriez appeler GetInvocationList() , qui vous permet d’appeler et d’ await chaque gestionnaire séparément:

     private async Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler != null) { var handlers = handler.GetInvocationList(); foreach (AsyncEventHandler innerHandler in handlers) { await innerHandler(this, mustLoadPreviousRunningState); } } } 

    Si vous savez que vous n’aurez jamais plus d’un gestionnaire, vous pouvez utiliser une propriété de délégué pouvant être définie directement au lieu d’un événement.

  2. Si vous avez une méthode async ou lambda qui a la seule await juste avant son return (et pas s finally ), alors vous n’avez pas besoin de la rendre async , retournez simplement la Task :

     Task.Factory.StartNew(() => this.OnDoLoading(true)) 

Tout d’abord, je vous recommande de revoir la conception de votre “événement asynchrone”.

Il est vrai que vous pouvez utiliser une valeur de retour de Task , mais il est plus naturel que les gestionnaires d’événements C # renvoient void . En particulier, si vous avez plusieurs abonnements, la Task renvoyée par le handler(this, ...) est uniquement la valeur renvoyée par l’ un des gestionnaires d’événements. Pour attendre correctement que tous les événements asynchrones soient terminés, vous devez utiliser Delegate.GetInvocationList avec Task.WhenAll lorsque vous Task.WhenAll l’événement.

Puisque vous êtes déjà sur la plate-forme WinRT, je vous recommande d’utiliser des “reports”. C’est la solution choisie par l’équipe WinRT pour les événements asynchrones. Elle devrait donc être familière aux consommateurs de votre classe.

Malheureusement, l’équipe WinRT n’a pas inclus l’infrastructure de report dans le framework .NET pour WinRT. J’ai donc écrit un article sur les gestionnaires d’événements asynchrones et sur la création d’un gestionnaire de report .

En utilisant un report, votre code de création d’événement ressemblerait à ceci:

 private Task OnDoLoading(bool mustLoadPreviousRunningState) { var handler = this.DoLoading; if (handler == null) return; var args = new DoLoadingEventArgs(this, mustLoadPreviousRunningState); handler(args); return args.WaitForDeferralsAsync(); } private Task PerformLoadingActionAsync() { TaskFactory uiFactory = new TaskFactory(_uiScheduler); // Trigger event on the UI thread. var eventHandlerTask = uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)).Unwrap(); Task commandTask = Task.Run(() => this.ExecuteCommand()); return Task.WhenAll(eventHandlerTask, commandTask); } 

Voilà donc ma recommandation pour une solution. Les avantages d’un report sont qu’il active les gestionnaires synchrones et asynchrones, qu’il s’agit d’une technique déjà familière aux développeurs WinRT et qu’il gère correctement plusieurs abonnés sans code supplémentaire.

Maintenant, pour savoir pourquoi le code original ne fonctionne pas, vous pouvez y penser en portant une attention particulière à tous les types de votre code et en identifiant ce que chaque tâche représente. Gardez à l’esprit les points importants suivants:

  • Task dérive de la Task . Cela signifie que la Task sera convertie en Task sans aucun avertissement.
  • StartNew n’est pas async , il se comporte donc différemment de Task.Run . Voir l’ excellent blog de Stephen Toub sur le sujet .

Votre méthode OnDoLoading renverra une Task représentant l’achèvement du dernier gestionnaire d’événements. Toute Task provenant d’autres gestionnaires d’événements est ignorée (comme je l’ai mentionné ci-dessus, vous devez utiliser Delegate.GetInvocationList ou des reports pour prendre en charge correctement plusieurs gestionnaires asynchrones).

Regardons maintenant PerformLoadingActionAsync :

 Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState)); 

Il y a beaucoup de choses dans cette déclaration. C’est sémantiquement équivalent à cette ligne de code (légèrement plus simple):

 Task evenHandlerTask = await uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)); 

OK, nous mettons donc en queue OnDoLoading sur le thread d’interface utilisateur. Le type de retour de OnDoLoading est Task , ainsi le type de retour de StartNew est Task . Le blog de Stephen Toub donne des détails sur ce type d’emballage , mais vous pouvez le concevoir comme OnDoLoading : la tâche “externe” représente le début de la méthode asynchrone OnDoLoading (jusqu’à ce qu’elle OnDoLoading céder un await ), et le ” “interne” représente l’ achèvement de la méthode asynchrone OnDoLoading .

Ensuite, nous await le résultat de StartNew . Cela décompresse la tâche “externe” et nous obtenons une Task qui représente la fin du OnDoLoading stocké dans evenHandlerTask .

 return Task.WhenAll(evenHandlerTask, commandTask); 

Vous retournez maintenant une Task qui représente le moment où commandTask et evenHandlerTask sont terminés. Cependant, vous utilisez une méthode async , votre type de retour actuel est donc Task – et c’est la tâche interne qui représente ce que vous voulez. Je pense que ce que vous vouliez faire était:

 await Task.WhenAll(evenHandlerTask, commandTask); 

Ce qui vous donnerait un type de retour de Task , représentant l’achèvement complet.

Si vous regardez comment ça s’appelle:

 this.PerformLoadingActionAsync().ContinueWith(...) 

ContinueWith agit sur la Task externe dans le code d’origine, alors que vous souhaitiez réellement que celle-ci agisse sur la Task interne .