Pattern d'appel asynchrone dans vos services

Tags: Thread

Imaginons une opération exécutée par un service qui risque de durer un certain temps (comme dirait Fernand Raynaud ;-))

Vous avez alors deux options pour l'exposition de votre méthode :

  • mode synchrone
  • mode asynchrone

Si vous êtes développeur chez Microsoft pour Windows 8, on vous dira que si votre méthode risque de durer plus de 50ms, alors vous devez impérativement exposer votre méthode de façon asynchrone. Et une bonne façon de nommer votre méthode, est de la suffixer par Async.

Qui dit asynchrone dit également comment le client va pouvoir consommer le résultat.

Approche évènementielle

Une solution envisagée par les premières versions du Framework .NET était d’être signifié par un évènement.

  • On créait une instance d’une classe,
  • On s’abonnait à un évènement de l’objet,
  • On appelait une méthode de la classe qui déclenchait l’évènement quand elle avait finie.

Exemple : System.Net.WebClient avec DownloadFileCompleted par exemple.

Approche incompréhensible

La 2ème approche consiste a utiliser le pattern recommandé par Microsoft (dès le Framework 1.0) qui repose sur le delegate AsyncCallback et l’interface IAsyncResult. Personnellement, malgré des années d’utilisation du framework, j’ai vraiment du mal avec ce pattern! (j’ai à chaque fois besoin d’une documentation pour me rappeler comment elle fonctionne).

  • Vous lancez votre méthode “longue” via un Beginxxx qui doit attendre dans ses arguments un AsyncCallBack
  • Dans le AsyncCallback, vous récupérez le résultat via un Endxxx sous forme de IAsyncResult qui possède le vrai résultat.

Bon, je n’irais pas plus loin…

Approche “moderne”

Cette fois, grâce aux dernières technologies implémentées dans le Framework, on va pouvoir simplifier grandement cela.

Notez que j’ai utilisé le mot Framework. Cela signifie que c’est possible dès le framework 4.0 qui intègre les nouvelles classes Task & co. Avec le futur Framework 4.5, je pourrais utiliser le mot Compilateur car en utilisant les termes “async” et “await”, toute la plomberie exposée ci-dessous sera implémentée.

Bref, ici, on est avec le Framework 4.0. Donc imaginons un service, une classe, avec une méthode qui prend un certain temps. La seule astuce consiste à créer une nouvelle méthode, attendant les même arguments mais qui, au lieu de retourner un TResult, retourne un Task<TResult>. Exemple :

public string GetFullName(string firstName, string lastName)
{
    return string.Format("{0} {1}", firstName, lastName);
}

public Task<string> GetFullNameAsync(string firstName, string lastName)
{
    // TODO
}

 

Il me semble (A CONFIRMER) que Microsoft avec Windows 8 va encore plus loin puisque dans notre cas, la méthode GetFullName sera private donc non exposée pour d’éventuels clients, parce qu’elle dépasse les fameux 50ms.

Remarquez que j’utilise comme convention de suffixer ma méthode par Async (l’idée ne vient pas de moi, je ne fais que copier les directives de MS Clignement d'œil). L’avantage, c’est qu’au moins on sait à quoi on a affaire.

Donc il nous faut maintenant implémenter notre méthode GetFullNameAsync.

Ici, mon code est simple, et même simplissime.

Mais vous pouvez imaginer que votre méthode GetFullName appelle par exemple un autre service qui interroge une base de données SQL Azure distante, donc qui potentiellement, risque de prendre du temps (non pas que SQL Azure soit lent, loin de moi l’idée, mais votre connexion réseau elle, peut très certainement avoir des ratés). Donc on pourrait dans ce cas écrire :

public Task<string> GetFullNameAsync(string firstName, string lastName)
{
    return Task<string>.Factory.StartNew(() => GetFullName(firstName, lastName));
}

C’est également ce que j’appellerai un cas simple car tout se fait dans le même thread.

Là ou cela se complique, c’est si votre méthode (GetFullName) appelle d’autres méthodes qui peuvent lancer d’autres threads. Votre autre service peut par exemple appeler en interne son repository en asynchrone également. Donc on risque d’avoir des enchainements de Task.

Heureusement dans le Framework, vous avez la classe qui gère cela pour vous, j’ai nommé : TaskCompletionSource.

Concrètement, cela donne ceci :

public Task<string> GetFullNameAsync(string firstName, string lastName)
{
    var tcs = new TaskCompletionSource<string>();

    // lancement asynchrone
    Task<string>.Factory
        .StartNew(() => GetFullName(firstName, lastName))
        .ContinueWith(
            task =>
            {
                // une erreur ?  
                if (task.IsFaulted)
                {
                    tcs.TrySetException(task.Exception.GetBaseException());
                }
                // annulé ?
                else if (task.IsCanceled)
                {
                    tcs.TrySetCanceled();
                }
                // on retourne le résultat 
                else
                {
                    tcs.TrySetResult(task.Result);
                }
            });

    return tcs.Task;
}

Remarquez l’utilisation des Tryxxx car il se peut qu’il y ai un appel concurrentiel de l’affectation du résultat, exception ,etc.

Pour le client cela devient simple (et c’est compatible avec les futurs async/await), ici avec resynchronisation dans le thread de l’UI :

service.GetFullNameAsync("Richard", "Clark")
    .ContinueWith(
       t => Console.WriteLine(t.Result),
       TaskScheduler.FromCurrentSynchronizationContext());
blog comments powered by Disqus