Poursuite de la tâche sur le thread d'interface utilisateur

214

Existe-t-il un moyen «standard» de spécifier qu'une continuation de tâche doit s'exécuter sur le thread à partir duquel la tâche initiale a été créée?

Actuellement, j'ai le code ci-dessous - cela fonctionne, mais garder une trace du répartiteur et créer une deuxième action semble être une surcharge inutile.

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Greg Sansom
la source
Dans le cas de votre exemple, vous pouvez utiliser Control.Invoke(Action), par exemple. TextBlock1.Invokeplutôt quedispatcher.Invoke
Colonel Panic
2
Merci @ColonelPanic, mais j'utilisais WPF (tel que balisé), pas winforms.
Greg Sansom

Réponses:

352

Appelez la suite avec TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

Cela ne convient que si le contexte d'exécution actuel se trouve sur le thread d'interface utilisateur.

Greg Sansom
la source
40
Son valide uniquement si le contexte d'exécution actuel est sur le thread d'interface utilisateur. Si vous placez ce code dans une autre tâche, vous obtenez InvalidOperationException (regardez la section Exceptions )
stukselbax
3
Dans .NET 4.5, la réponse de Johan Larsson devrait être utilisée comme moyen standard pour la poursuite d'une tâche sur le thread d'interface utilisateur. Il suffit d'écrire: attendre Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Terminé"; Voir aussi: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W
1
Merci d'avoir sauvé ma vie. Je passe des heures à comprendre comment appeler les choses du thread principal dans l'attente / ContinueWith. Pour tout le monde, comment utilise le SDK Google Firebase pour Unity et a toujours les mêmes problèmes, c'est une approche qui fonctionne.
CHaP
2
@MarcelW - awaitest un bon modèle - mais seulement si vous êtes dans un asynccontexte (comme une méthode déclarée async). Sinon, il est toujours nécessaire de faire quelque chose comme cette réponse.
ToolmakerSteve
33

Avec async, vous faites simplement:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

Toutefois:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Johan Larsson
la source
2
Le commentaire sous la falseversion me confond. Je pensais falseque cela pouvait continuer sur un autre thread.
ToolmakerSteve
1
@ToolmakerSteve Dépend du thread auquel vous pensez. Le thread de travail utilisé par Task.Run ou le thread d'appelant? Rappelez-vous, "même thread sur lequel la tâche s'est terminée" signifie le thread de travail (en évitant de "basculer" entre les threads). De plus, ConfigureAwait (true) ne garantit pas que le contrôle retourne au même thread , uniquement au même contexte (bien que la distinction ne soit pas significative).
Max Barraclough
@MaxBarraclough - Merci, j'ai mal lu le "même fil". éviter de basculer entre les threads dans le sens de maximiser les performances en utilisant le thread en cours d'exécution [pour effectuer la tâche "faire quelques trucs"], cela me le clarifie.
ToolmakerSteve
1
La question ne précise pas être à l'intérieur d'une asyncméthode (ce qui est nécessaire, à utiliser await). Quelle est la réponse quand awaitn'est pas disponible?
ToolmakerSteve
22

Si vous avez une valeur de retour que vous devez envoyer à l'interface utilisateur, vous pouvez utiliser la version générique comme ceci:

Ceci est appelé à partir d'un ViewModel MVVM dans mon cas.

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());
Simon_Weaver
la source
Je suppose que le = avant GenerateManifest est une faute de frappe.
Sebastien
Oui - c'est parti maintenant! THX.
Simon_Weaver
11

Je voulais juste ajouter cette version car c'est un fil tellement utile et je pense que c'est une implémentation très simple. J'ai utilisé cela plusieurs fois dans différents types si l'application multithread:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
doyen
la source
2
Pas de vote en aval car il s'agit d'une solution viable dans certains scénarios; cependant, la réponse acceptée est bien meilleure. Il est indépendant de la technologie ( TaskSchedulerfait partie de BCL, Dispatchern'est pas le cas) et peut être utilisé pour composer des chaînes de tâches complexes car il ne doit pas se soucier d'opérations asynchrones de type "ignorer et oublier" (comme BeginInvoke).
Kirill Shlenskiy
@Kirill pouvez vous développer un peu, car certains threads SO ont déclaré à l'unanimité que le répartiteur est la bonne méthode si vous utilisez WPF de WinForms: on peut invoquer une mise à jour de l'interface graphique soit de manière asynchrone (en utilisant BeginInvoke) ou de manière synchrone (Invoke), bien que généralement asynchrone est utilisé car on ne voudrait pas bloquer un thread d'arrière-plan uniquement pour une mise à jour de l'interface graphique. FromCurrentSynchronizationContext ne place-t-il pas la tâche de continuation dans la file d'attente de messages du thread principal de la même manière que le répartiteur?
Dean
1
Oui, mais l'OP pose certainement des questions sur WPF (et l'a étiqueté ainsi), et ne veut pas garder de référence à aucun répartiteur (et je suppose que tout contexte de synchronisation non plus - vous ne pouvez l'obtenir que depuis le thread principal et vous devez stocker une référence quelque part). C'est pourquoi j'aime la solution que j'ai publiée: il existe une référence statique thread-safe intégrée qui ne nécessite rien de tout cela. Je pense que cela est extrêmement utile dans le contexte WPF.
Dean
3
Je voulais juste renforcer mon dernier commentaire: le développeur doit non seulement stocker le contexte de synchronisation, mais il / elle doit savoir que cela n'est disponible que depuis le fil principal; ce problème a été la cause de la confusion dans des dizaines de questions SO: Les gens essaient tout le temps de l'obtenir à partir du thread de travail. Si leur code a lui-même été déplacé dans un thread de travail, il échoue en raison de ce problème. Donc, en raison de la prévalence du WPF, cela devrait certainement être clarifié ici dans cette question populaire.
Dean
1
... néanmoins, l'observation de Dean sur [la réponse acceptée] ayant besoin de garder une trace du contexte de synchronisation si le code pourrait ne pas être sur le thread principal est importante à noter, et éviter cela est un avantage de cette réponse.
ToolmakerSteve
1

Je suis arrivé ici via Google parce que je cherchais un bon moyen de faire des choses sur le thread d'interface utilisateur après avoir été à l'intérieur d'un appel Task.Run - En utilisant le code suivant, vous pouvez utiliser awaitpour revenir au thread d'interface utilisateur.

J'espère que ça aidera quelqu'un.

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

Usage:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...
Dbl
la source
1
Très intelligent! Assez intuitif cependant. Je suggère de faire staticle cours UI.
Theodor Zoulias