[C# 5] Programmation asynchrone avec C# 5

Depuis quelque temps, les spéculations allaient bon train sur les fonctionnalités de la future version 5 du langage C#… Très peu d’informations officielles avaient filtré à ce sujet, la seule chose plus ou moins certaine était l’introduction du concept de “compilateur en temps que service”, qui permettrait de tirer parti du compilateur à partir du code. A part ça, silence radio de la part de Microsoft…

Lors de la PDC jeudi dernier, un coin du voile a enfin été levé, mais pas du tout sur ce qu’on attendait ! Anders Hejlsberg, le créateur de C#, a bien consacré quelques minutes à la notion de “compiler as a service”, mais l’essentiel de sa présentation portait sur quelque chose de complètement différent : la programmation asynchrone en C#.

Il est bien sûr déjà possible d’effectuer des traitements asynchrones en C#, mais c’est généralement assez pénible et peu intuitif… On est souvent obligé de passer par des callbacks pour indiquer ce qui doit être exécuté à la fin du traitement asynchrone, et on se retrouve rapidement avec un code difficile à relire et à comprendre, et donc à maintenir. Pour une démonstration de ce problème, je vous invite à lire l’excellent article d’Eric Lippert à ce sujet, il explique ça beaucoup mieux que moi…

Cet article (et la série qu’il conclut) était en fait un prélude à l’annonce faite à la PDC : C# 5 intègrera une nouvelle syntaxe permettant d’écrire du code asynchrone de façon beaucoup plus naturelle, avec l’introduction de deux nouveaux mots-clés : async et await. Le code à écrire pour réaliser un traitement asynchrone sera quasiment identique à celui d’un traitement synchrone : toute la complexité sera masquée par cette nouvelle fonctionnalité du langage.

Puisqu’un exemple vaut mieux qu’un long discours, je vais reprendre l’exemple utilisé par Anders Hejlsberg pendant sa présentation, en le simplifiant un peu. Supposons qu’on veuille rechercher des titres de films par leur année de sortie. Pour simplifier, on utilisera le service OData de Netflix. Le code suivant effectue la recherche de façon synchrone, en récupérant les résultats 10 par 10 :

        private void btnSearch_Click(object sender, RoutedEventArgs e)
        {
            int year;
            if (!int.TryParse(txtYear.Text, out year))
            {
                MessageBox.Show("L'année saisie est incorrecte");
                return;
            }
            SearchMovies(year);
        }

        private void SearchMovies(int year)
        {
            var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
            var catalog = new Netflix.NetflixCatalog(netflixUri);
            lstTitles.Items.Clear();
            int count = 0;
            int pageSize = 10;
            while (true)
            {
                var movies = SearchMoviesBatch(catalog, year, count, pageSize);
                if (movies.Length == 0)
                    break;
                foreach (var title in movies)
                {
                    lstTitles.Items.Add(title.Name);
                }
                count += movies.Length;
            }
        }

        private Title[] SearchMoviesBatch(NetflixCatalog catalog, int year, int count, int pageSize)
        {
            var query = from title in catalog.Titles
                            where title.ReleaseYear == year
                            orderby title.Name
                            select title;
            return query.Skip(count).Take(pageSize).ToArray();
        }

Ce code a le mérite d’être assez simple, mais il suffit de l’exécuter pour se rendre compte qu’il y a un problème : la récupération des résultats peut prendre un certain temps, pendant lequel l’interface reste figée. Il faut donc effectuer la recherche de façon asynchrone, pour que l’interface reste réactive. Voici une approche possible, avec la version actuelle de C# :

        private void SearchMoviesAsync(int year)
        {
            lstTitles.Items.Clear();
            Thread t = new Thread(() =>
            {
                var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
                var catalog = new Netflix.NetflixCatalog(netflixUri);
                int count = 0;
                int pageSize = 10;
                while (true)
                {
                    var movies = SearchMoviesBatch(catalog, year, count, pageSize);
                    if (movies.Length == 0)
                        break;
                    foreach (var title in movies)
                    {
                        Dispatcher.Invoke(new Action(() => lstTitles.Items.Add(title.Name)));
                    }
                    count += movies.Length;
                }
            });
            t.Start();
        }

(Les deux autres méthodes sont inchangées)

On voit que le code commence déjà à être moins clair, à cause de l’expression lambda passée au constructeur du thread, et de l’utilisation de Dispatcher.Invoke pour mettre à jour l’interface graphique. Imaginez un peu ce que ça donnerait dans un scénario plus complexe, avec plusieurs tâches asynchrones interdépendantes (comme dans l’article d’Eric Lippert mentionné plus haut).

Avec la nouvelle syntaxe introduite par C# 5, voici comment on pourrait écrire ce code :

        private async void SearchMoviesAsync(int year)
        {
            var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
            var catalog = new Netflix.NetflixCatalog(netflixUri);
            lstTitles.Items.Clear();
            int count = 0;
            int pageSize = 10;
            while (true)
            {
                var movies = await SearchMoviesBatchAsync(catalog, year, count, pageSize);
                if (movies.Length == 0)
                    break;
                foreach (var title in movies)
                {
                    lstTitles.Items.Add(title.Name);
                }
                count += movies.Length;
            }
        }

        private async Task<Title[]> SearchMoviesBatchAsync(NetflixCatalog catalog, int year, int count, int pageSize)
        {
            var query = from title in catalog.Titles
                        where title.ReleaseYear == year
                        orderby title.Name
                        select title;
            return await query.Skip(count).Take(pageSize).ToArrayAsync();
        }

Remarquez que les deux méthodes ont un nouveau modificateur async, qui indique qu’elles s’exécutent de façon asynchrone. Lors de l’appel à une autre méthode asynchrone, l’appel est précédé du mot-clé await. Lorsque la méthode SearchMoviesAsync est appelée, elle commence à s’exécuter normalement, jusqu’au mot-clé await. A partir de là, deux scénarios sont possibles

  • soit l’appel à SearchMoviesBatchAsync se termine de façon synchrone, auquel cas l’exécution continue normalement
  • soit il s’exécute de façon asynchrone, dans ce cas le contrôle est rendu à la méthode qui appelle SearchMoviesAsync (en l’occurrence btnSearch_Click). Quand l’appel à SearchMoviesBatchAsync se termine, l’exécution de SearchMoviesAsync reprend là où elle en était (de ce point de vue, await fonctionne un peu comme yield return)

Un peu comme pour les blocs itérateurs, le compilateur réécrit le code de la méthode en créant un delegate avec le code qui suit l’appel asynchrone, et appelle ce delegate quand la tâche asynchrone se termine. Remarquez d’ailleurs que ce delegate est appelé sur le même thread, celui du dispatcher en l’occurrence : on n’a donc pas besoin de Dispatcher.Invoke pour mettre à jour l’interface graphique.

En pratique, tout ce système se base sur la classe Task introduite dans .NET 4. Remarquez d’ailleurs que le type de retour de la méthode SearchMoviesBatchAsync est Task<Title[]>. Pourtant, quand on appelle cette méthode à partir de SearchMoviesAsync, on récupère bien un objet de type Title[], et non Task<Title[]>. C’est l’autre effet du mot-clé await : il récupère le résultat d’une tâche une fois qu’elle est terminée.

Encore une chose : j’ai utilisé dans le code une méthode d’extension ToArrayAsync, voici son code :

        public static Task<T[]> ToArrayAsync<T>(this IQueryable<T> source)
        {
            return TaskEx.Run(() => source.ToArray());
        }

Voilà pour l’introduction à cette future nouvelle fonctionnalité de C#. J’espère que c’était à peu près compréhensible et que je n’ai pas dit trop de bêtises… tout n’est pas encore complètement clair dans ma têteClignement d'œil. Pour en savoir plus, voici quelques liens utiles :

Pour les adeptes de VB.NET, sachez que cette fonctionnalité sera aussi inclue dans la prochaine version de Visual Basic, ainsi que les itérateurs, qui n’existaient qu’en C# jusqu’à maintenant.

7 thoughts on “[C# 5] Programmation asynchrone avec C# 5”

  1. Intéressant.

    Par contre, hormis cacher le désossage du résultat de l”AsyncResult, quel avantage par rapport à un Action “classique” ?

    Merci.

    1. L”avantage principal est que le code se présente de façon séquentielle, exactement comme du code purement synchrone. C”est donc beaucoup plus facile de voir le déroulement des opérations que lorsqu”on utilise des callbacks dans tous les sens.

      A vrai dire je ne suis pas très satisfait de mon exemple, il n”est pas aussi parlant que je l”aurais souhaité… pour une meilleure illustration et des explications plus complètes, je vous conseille vivement la lecture des articles d”Eric Lippert que j”ai mentionnés.

  2. Ne serait-il pas plus judicieux d”inventer un nouvel opérateur plutôt de rajouter les 2 mots clés async et await ?
    De plus le nommage des fonctions avec le rajout “Async” deviendrait inutile et bien meilleur.
    Ainsi un : async Task SearchMoviesBatchAsync();
    deviendrait un simple : Title[]” SearchMoviesBatch();
    et un : var movies = await SearchMoviesBatchAsync();
    deviendrait : var movies = SearchMoviesBatchAsync()”;

    Oui j”ai décidé que le ” ” ” serait un opérateur 😀

    1. C”est plutôt aux concepteurs du langage qu”il faudrait faire cette suggestion 😉

      Personnellement je préfère largement les mots-clés. Quelqu”un qui ne connait pas encore cette nouvelle fonctionnalité du langage comprendra tout de suite, en voyant le mot-clé async, le caractère asynchrone de la méthode. En plus, un opérateur aussi discret que ` risque de passer inaperçu alors qu”il a des conséquences importantes !

    2. discret… discret… le caractère ” ” ” n”est p”être pas le plus approprié… il reste à trouver le meilleur 😉
      Pour moi un opérateur passerait aussi inaperçu que peut l”être l”opérateur “*” du pointeur 😀
      Et puis comme les fonctions asynchrones seront de plus en plus utilisées, voire indispensables… une écriture simple, robuste et efficace serait souhaitable

      Je vais demander à Microsoft de me l”implanter pour demain 😮

Leave a Reply

Your email address will not be published. Required fields are marked *