Accueil > CSharp > Dispatcher.SwitchTo()

Dispatcher.SwitchTo()

Thread, SynchronizationContext et autres mécanismes asynchrones mis sur le devant de la scène des interfaces fluides et réactives restent des objets complexes aux fonctionnement souvent mal compris.

Depuis C# 5, les choses se simplifient incroyablement au niveau de la syntaxe, le code apparaissant quasiment comme synchrone.

J’ai souvent traité de ce sujet passionnant, comme lors du Techdays 2014. J’y avais notamment détaillé le rôle des Awaiters qui sont à la base des mécanismes de continuation de code (await).

Pas de classe de base ni d’interface pour prétendre pouvoir être “awaité” mais uniquement une convention: implémenter une méthode publique GetAwaiter() retournant un objet implémentant l’interface INotifyCompletion

Les classes Task et TaskAwaiter du framework .Net offre une version prête à l’emploi de ces mécanismes et pour le commun des développeurs, apprendre à appréhender la classe Task sera largement suffisant pour implémenter un maximum de scénarios asynchrones.

Pour autant, certaines tâches plus complexes ou demandant une maitrise plus fine nécessiteront tout de même la mise en œuvre de nos propres Awaiters.

Je vous propose donc de faire quelques rappels ici avant de démontrer une solution plus complexe dans un billet ultérieur.

Pour rappel, c’est un exemple que j’avais présenté lors de la session des Techdays 2014 “C# Async, un après” : https://www.youtube.com/watch?v=M8ypKMorCsU

Ainsi pour pouvoir utiliser le mot clé “await”, il faut le faire suivre d’un objet exposant une méthode GetAwaiter(). Finesse extrême du compilateur C#, cette méthode peut être une méthode d’extension ! Ceci est très bien vu car on peut donc rendre “awaitable” d’anciennes classes ou des classes dont nous n’aurions pas les sources.

public static class AsyncExtensions
{
    public static TaskAwaiter GetAwaiter(this Int32 millisecond)
    {
        return Task.Delay(millisecond).GetAwaiter();
    }
    public static TaskAwaiter GetAwaiter(this DateTime time)
    {
        return Task.Delay((int)(time - DateTime.Now).TotalMilliseconds).GetAwaiter();
    }
}

Le code ci-dessus nous montre comment rendre le type “int” et “datetime” awaitables.

Nous leur ajoutons par extension une méthode “GetAwaiter” qui sera correctement résolue par le compilateur C# lorsqu’elle sera combinée à un await.

Vous remarquerez que dans cet exemple, je ne redéveloppe pas mon propre Awaiter mais je combine l’awaiter de Task.Delay().

Je peux désormais appeler les codes suivants qui sont équivalents :

await Task.Delay(1000);
await 1000;
await (DateTime.Now + TimeSpan.FromMilliseconds(1000));


La suite concerne WPF mais peut-être imaginée dans d’autres contextes de synchronisation de threads.

Dans le code suivant, une majorité de développeurs connaissent le problème, il n’est pas possible d’accéder à un élément graphique de WPF depuis un thread autre que le thread de rendu: ici l’accès à la propriété Title de la fenêtre depuis un thread du pool soulèvera une exception.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Task.Run(() =>
        {
            Title = "ok";
        });
    }
}

Dans WPF, le contexte d’exécution relatif à une fenêtre est simplifié au travers d’une classe appelée Dispatcher, nul besoin donc de faire appel aux classes Thread ou SynchronizationContext dans la plupart des cas. Le Dispatcher, comme son nom l’indique encapsule la logique de thread de rendu (UI) et de pompe de message Windows. On s’en sert principalement pour resynchroniser un code venant d’un autre thread.

Tous les objets WPF liés au graphique dérivent de DispatcherObject. Cette classe référence le Dispatcher de rendu et expose les méthodes CheckAccess et VerifyAccess pour vérifier que vous êtes bien dans le bon thread de rendu. Tous les contrôles graphiques appellent cette méthode avant de modifier quoi que ce soit. Le nombre d’appels à cette méthode est phénoménal mais c’est le seul moyen d’avoir une exception propre et interceptable contrairement aux appels GDI qui plantent l’application “nativement”, même en Windows Forms.

La résolution de notre problème est donc relativement simple grâce au Dispatcher qui expose des méthodes de synchronisation. Il existe même une récente méthode (.Net 4.5 ?) supportant l’await. (InvokeAsync)

private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(() =>
    {
        Dispatcher.BeginInvoke((Action)(() => Title = "ok"));
    });

    Task.Run(() =>
    {
        Dispatcher.InvokeAsync(() => Title = "ok");
    });
}


Jusque là quasi tout le monde connait. Je vous propose ici une autre solution sans avoir à utiliser d’action mais en personnalisant les mécanismes de code de continuation.

Vous avez surement déjà entendu “avec await, le code de continuation reprend son cours dans le même thread que le code appelant’’. Cela est très important et très appréciable car cela contribue à “simuler” un code synchrone.

Ainsi le code suivant l’instruction “await Task.Delay(1000)” continue dans le thread courant. Cette assertion n’est pourtant pas toujours vraie… Ceci n’est assuré que si vous êtes dans le thread de rendu. Pourquoi donc ? Car le thread de rendu bénéficie de la boucle principale de messages qui sera capable à un moment ou un autre de redonner la main au code de continuation.

Si ce code est appelé dans une application Console ou dans une page ASP.Net, le code de continuation sera exécuté par une thread du pool.

Si l’on préfère continuer dans un thread du pool même en étant dans un thread UI, on peut appeler: await Task.Delay(1000).ConfigureAwait(false)

Ceci aura pour effet de ne pas capturer le contexte appelant (et donc de retour).

Revenons à notre solution plus simple pour la synchronisation au thread de rendu. Mon but est que le code de continuation soit exécuté par le thread du Dispatcher et non dans une action passée en paramètre. On condamne du coup la possibilité de revenir au thread appelant. Cette solution convient donc au cas où l’on veut terminer notre méthode dans le thread de rendu.

Premièrement et comme montré précédemment, ajoutons la méthode d’extension “SwitchTo” à la classe Dispatcher. Par contre cette fois, nous allons développer notre propre Awaiter et notre propre implémentation d’INotifyCompletion !

 

public static class DispatcherExtensions
{
    public static DispatcherSynchronizer SwitchTo(this Dispatcher dispatcher, 
        DispatcherPriority priority = DispatcherPriority.Normal)
    {
        return new DispatcherSynchronizer(dispatcher, priority);
    }
}
    
public class DispatcherSynchronizer
{
    private DispatcherSynchronizerAwaiter awaiter;

    public DispatcherSynchronizer(Dispatcher dispatcher,
        DispatcherPriority priority)
    {
        awaiter = new DispatcherSynchronizerAwaiter(dispatcher, priority);
    }
    public DispatcherSynchronizerAwaiter GetAwaiter()
    {
        return awaiter;
    }
    public struct DispatcherSynchronizerAwaiter : INotifyCompletion
    {
        private Dispatcher dispatcher;
        private DispatcherPriority priority;
        public object GetResult()
        {
            return null;
        }
        public bool IsCompleted
        {
            get { return false; }
        }
        public DispatcherSynchronizerAwaiter(Dispatcher dispatcher, 
            DispatcherPriority priority)
        {
            this.dispatcher = dispatcher;
            this.priority = priority;
        }
        void INotifyCompletion.OnCompleted(Action continuation)
        {
            if (Dispatcher.CurrentDispatcher == dispatcher)
                continuation();
            else
                dispatcher.InvokeAsync(continuation, priority);
        }
    }
}

 

Le code intéressant se trouve dans la classe DispatcherSynchronizerAwaiter.

Dans notre cas, pas de valeur de retour, l’awaiter est non complété sinon OnCompleted n’est pas appelé. Je conserve à la construction de l’objet une référence sur le fameux Dispatcher.

Nous voilà enfin dans le OnCompleted ! (bravo si vous avez tenu jusque là, oui c’est un peu long)

Le paramètre “continuation” est l’Action de continuation, il nous suffit donc de l’appeler dans le contexte voulu. Si je suis déjà dans le bon Dispatcher, j’exécute l’action de manière synchrone tout simplement, sinon je synchronise grâce à InvokeAsync qui attend…une action !

Le code devient donc:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(async () =>
    {
        await Dispatcher.SwitchTo();
        Title = "ok";
    });
}

Pas de paramètre à passer, pas de lambda. Le code de continuation s’exécute dans le thread de rendu !!

Si vous voulez revoir tout cela en image, je parle de ces mécanismes dans la session des Techdays 2014 référencée plus haut dans cet article.

Bonne lecture à tous

Catégories :CSharp Étiquettes : , ,
  1. 03/08/2015 à 17:57

    Excellente idée! J’ai essayé récemment de faire une sorte d’opposée/de dual: comment retrouver l’option RunSynchronously avec async/await. A priori, ce n’est pas possible: voir http://stackoverflow.com/questions/30919601/async-await-vs-hand-made-continuations-is-executesynchronously-cleverly-used

    Un challenge pour un maître ?

  2. 04/08/2015 à 14:47

    J’ai pas tout compris… Le RunSynchronously de ContinueWith() est conditionnel suivant l’état de la tache précédente. Si l’on se place après un await, la Task est non seulement terminée mais n’est plus accessible. Non ?

  3. 11/08/2015 à 10:46

    Mon but était, qu’en utilisant TaskContinuationOptions.ExecuteSynchronously (voir l’explication de Stephen Toub ici: blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx), la continuation évite tout « Enqueing »: qu’elle s’exécute directement dans la foulée de la précédente par le thread qui l’a exécuté (sous réserve des cas limites expliqués par ST bien sûr).

  1. No trackbacks yet.

Laisser un commentaire