Tag Archives: async

Méthodes C# dans les en-têtes de diff git

Si vous utilisez git en ligne de commande, vous aurez peut-être remarqué que les diffs indiquent souvent la signature de la méthode dans l’en-tête du bloc (la ligne qui commence par @@), comme ceci :

diff --git a/Program.cs b/Program.cs
index 655a213..5ae1016 100644
--- a/Program.cs
+++ b/Program.cs
@@ -13,6 +13,7 @@ static void Main(string[] args)
         Console.WriteLine("Hello World!");
         Console.WriteLine("Hello World!");
         Console.WriteLine("Hello World!");
+        Console.WriteLine("blah");
     }

C’est très pratique pour savoir où vous vous trouvez quand vous regardez un diff.

Git a quelques patterns d’expressions régulières prédéfinis pour détecter les méthodes dans quelques langages, y compris C#; ils sont définis dans userdiff.c. Mais par défaut, ces patterns ne sont pas utilisés… il faut dire à git quelles extensions de fichier sont associées à quel langage. Cela peut être spécifié dans un fichier .gitattributes à la racine de votre repo :

*.cs    diff=csharp

Cela fait, git diff devrait afficher une sortie similaire à l’exemple ci-dessus.

Est-ce que ça suffit ? Presque. En fait, les patterns pour C# ont été ajoutés à git il y a longtemps, et C# a pas mal évolué depuis. Certains nouveaux mots-clés qui peuvent maintenant apparaître dans une signature de méthode ne sont pas reconnus par le pattern prédéfini, par exemple async ou partial. C’est assez agaçant, parce que quand du code a changé dans une méthode asynchrone, l’en-tête du bloc dans le diff indique la signature de la précédente méthode non-asynchrone, ou la ligne où la classe est déclarée, ce qui prête à confusion.

Mon premier réflexe a été de soumettre une pull request sur Github pour ajouter les mots-clés manquants ; cependant j’ai vite réalisé que le repo de git sur Github est juste un miroir et n’accepte pas de pull requests… Le processus de contribution consiste à envoyer un patch à la mailing list de git, avec une longue et ennuyeuse liste de règles à respecter. Ce processus m’a semblé si laborieux que j’ai abandonné l’idée. Franchement, je ne sais pas pourquoi git utilise un processus de contribution aussi obsolète et compliqué, ça ne fait que décourager les contributeurs occasionnels. Mais c’est un peu hors-sujet, alors passons et voyons si on peut résoudre le problème autrement.

Heureusement, les patterns prédéfinis peuvent être redéfinis dans la configuration de git. Pour définir le pattern de signature de fonction pour C#, il faut définir le paramètre diff.csharp.xfuncname dans votre fichier de configuration git :

[diff "csharp"]
  xfuncname = ^[ \\t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe|async|partial)[ \\t]+)*[][<>@.~_[:alnum:]]+[ \\t]+[<>@._[:alnum:]]+[ \\t]*\\(.*\\))[ \\t]*$

Comme vous pouvez le voir, c’est le même pattern que dans userdiff.c, avec les backslashes échappés et les mots-clés manquants ajoutés. Avec ce pattern, git diff affiche maintenant la signature correcte pour les méthodes asynchrones :

diff --git a/Program.cs b/Program.cs
index 655a213..5ae1016 100644
--- a/Program.cs
+++ b/Program.cs
@@ -31,5 +32,6 @@ static async Task FooAsync()
         Console.WriteLine("Hello world");
         Console.WriteLine("Hello world");
         Console.WriteLine("Hello world");
+        await Task.Delay(100);
     }
 }

Ça m’a pris un moment de trouver comment le faire marcher, alors j’espère que ça vous sera utile !

Piège: utiliser var et async ensemble

Il y a quelques jours au bureau, je suis tombé sur un bug assez sournois dans notre application principale. Le code semblait assez innocent, et à première vue je ne voyais vraiment pas ce qui n’allait pas… Le code était similaire à ceci:

public async Task<bool> BookExistsAsync(int id)
{
    var store = await GetBookStoreAsync();
    var book = store.GetBookByIdAsync(id);
    return book != null;
}

// Pour donner le contexte, voici les types et méthodes utilisés dans BookExistsAsync:

private Task<IBookStore> GetBookStoreAsync()
{
    // ...
}


public interface IBookStore
{
    Task<Book> GetBookByIdAsync(int id);
    // ...
}

public class Book
{
    public int Id { get; set; }
    // ...
}

La méthode BookExistsAsync renvoie toujours true. Voyez-vous pourquoi ?

Regardez cette ligne :

var book = store.GetBookByIdAsync(id);

Vite, sans réfléchir, quel est le type de book ? Si vous avez répondu Book, regardez de plus près : c’est Task<Book>. Il manque le await ! Et une méthode async renvoie toujours une tâche non nulle, donc book n’est jamais nul.

Quand vous avez une méthode async sans await, le compilateur vous avertit, mais en l’occurrence il y a un await sur la ligne précédente. La seule chose qu’on fait avec book est de vérifier qu’il n’est pas null ; puisque Task<T> est un type référence, il n’y rien de suspect dans le fait de le comparer à null. Donc le compilateur ne voit rien d’anormal ; l’analyseur statique de code (ReSharper dans mon cas) ne voit rien d’anormal ; et le pauvre cerveau humain qui fait la revue de code ne voit rien d’anormal non plus… Évidemment, le problème aurait facilement pu être détecté avec une couverture de tests adéquate, mais malheureusement cette méthode n’était pas couverte.

Alors, comment éviter ce type d’erreur ? En arrêtant d’utiliser var et en spécifiant toujours les types explicitement ? Mais j’aime var, je l’utilise presque partout ! D’ailleurs, je crois que c’est la première fois que je vois un bug lié à l’utilisation de var. Je ne suis vraiment pas prêt à l’abandonner…

Idéalement, j’aurais aimé que ReSharper détecte le problème; peut-être qu’il devrait considérer que toutes les méthodes qui renvoient une Task sont implicitement [NotNull], sauf mention contraire. En attendant, je n’ai pas de solution miracle pour ce problème ; faites juste attention quand vous appelez une méthode asynchrone, et écrivez des tests unitaires !

Utiliser plusieurs sources d’annulation avec CreateLinkedTokenSource

La programmation asynchrone en C# était auparavant quelque chose de difficile ; grâce à la Task Parallel Library de .NET 4 et au async/await de C# 5, elle est devenu relativement facile, et en conséquence, est de plus en plus couramment utilisée. Dans le même temps, une approche standardisée pour gérer l’annulation a été introduite : les jetons d’annulation. L’idée générale est que vous créez un CancellationTokenSource qui contrôle l’annulation, et vous passez le jeton qu’il fournit à la méthode que vous voulez pouvoir annuler. Cette méthode le passera ensuite aux autres méthodes qu’elle appelle si elles peuvent être annulées, et/où vérifiera régulièrement si l’annulation a été demandée. Lors de l’annulation, la méthode lancera une OperationCanceledException. Exemple vite fait mal fait :

private readonly IBusinessService _businessService;
private CancellationTokenSource _cancellationSource;
private Task _asyncOperation;
 
private void StartAsyncOperation()
{
    if (_asyncOperation != null)
        return;
    var _cancellationSource = new CancellationTokenSource();
    _asyncOperation = _businessService.DoSomethingAsync(_cancellationSource.Token);
}
 
// async void is bad; like I said, this is a quick and dirty example
private async void StopAsyncOperation()
{
    try
    {
        _cancellationSource.Cancel();
        // wait for the operation to finish
        await _asyncOperation;
    }
    catch (OperationCanceledException)
    {
        // Operation was successfully canceled
    }
    catch (Exception)
    {
        // Oops, something went wrong
    }
    finally
    {
        _asyncOperation = null;
        _cancellationSource.Dispose();
        _cancellationSource = null;
    }
 
...
 
class BusinessService : IBusinessService
{
    public async Task DoSomethingAsync(CancellationToken cancellationToken)
    {
        var data = await GetDataFromServerAsync(cancellationToken);
        foreach (string line in data)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await ProcessLineAsync(line, cancellationToken);
        }
    }
 
    ...
}

Dans ce cas, StopAsyncOperation serait appelée, par exemple, si l’utilisateur décide d’annuler l’opération.

Tout ça marche très bien et est assez facile à mettre en œuvre. Mais que se passe-t-il s’il y a une autre raison d’annuler l’opération, connue seulement du BusinessService et hors du contrôle de la méthode appelante ? C’est là que la méthode CancellationSource.CreateLinkedTokenSource entre en jeu ; en gros, cette méthode crée une nouvelle source d’annulation qui sera annulée quand l’un des jetons spécifiés sera annulé.

Commençons par un cas simple : vous avez un autre jeton d’annulation que vous voulez aussi prendre en compte. Le code pourrait ressembler à ceci :

public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
    var otherToken = GetOtherCancellationToken();
    using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, otherToken))
    {
        var data = await GetDataFromServerAsync(linkedCts.Token);
        foreach (string line in data)
        {
            linkedCts.Token.ThrowIfCancellationRequested();
            await ProcessLineAsync(line, linkedCts.Token);
        }
    }
}

On crée une source d’annulation liée, basée sur les deux jetons d’annulation, et on utilise le jeton de cette nouvelle source à la place de cancellationToken. Si cancellationToken ou otherToken est annulé, linkedCts.Token sera annulé aussi. Si nécessaire, le code appelant peut détecter comment l’opération a été annulée en vérifiant la propriété CancellationToken de l’exception OperationCanceledException.

Maintenant, un cas un peu plus difficile : la seconde source d’annulation est en fait un évènement. On veut annuler l’opération quand cet évènement se produit, en plus de l’annulation par l’utilisateur représentée par le paramètre cancellationToken. On va donc s’abonner à l’évènement et déclencher l’annulation quand il se produit. Voilà un moyen de le faire :

public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
    using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
    {
        EventHandler handler = (sender, e) => linkedCts.Cancel();
        try
        {
            SomeEvent += handler;
            var data = await GetDataFromServerAsync(linkedCts.Token);
            foreach (string line in data)
            {
                linkedCts.Token.ThrowIfCancellationRequested();
                await ProcessLineAsync(line, linkedCts.Token);
            }
        }
        finally
        {
            SomeEvent -= handler;
        }
    }
}

Ici on passe seulement cancellationToken à CreateLinkedTokenSource, et on annule directement linkedCts quand l’évènement se produit. Le code devient un peu plus alambiqué, mais il atteint l’objectif.

Je ne peux pas vraiment vous donner un cas d’utilisation spécifique de cette technique dans le monde réel, parce que les cas où je l’ai utilisée sont trop spécifiques pour être d’intérêt général, mais je peux décrire le scénario général. J’ai une opération de longue durée qui est constituée de plusieurs sous-opérations de longue durée. L’opération entière peut être annulée globalement, et chacune des sous-opérations peut également être annulée individuellement, sans affecter les autres. Voilà à quoi ça peut ressembler (en quasi pseudo-code) :

async Task GlobalOperationAsync(CancellationToken cancellationToken)
{
    foreach (var subOperation is SubOperations)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var subToken = subOperation.GetSpecificCancellationToken();
        using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, subToken))
        {
            try
            {
                await subOperation.RunAsync(linkedCts.Token);
            }
            catch (OperationCanceledException ex)
            {
                // Rethrow only if global cancellation was requested
                if (cancellationToken.IsCancellationRequested)
                    throw;
                     
                // otherwise continue running the other sub-operations
            }
        }
    }
}

Remarquez que, bien que CancellationToken ait été introduit avec la TPL et que tous mes exemples soient asynchrones, vous pouvez également utiliser cette technique avec du code synchrone.

Voilà, j’espère que cela vous sera utile. Passez un bon réveillon du nouvel an, et une excellente année 2016 !

Support de l’asynchronisme et de l’annulation pour les wait handles

Le .NET Framework fournit un certain nombre de primitives de synchronisation bas niveau. Les plus couramment utilisées sont appelées “wait handles”, et héritent de la classe WaitHandle : Semaphore, Mutex, AutoResetEvent et ManualResetEvent. Ces classes existent depuis .NET 2.0 (voire 1.1 pour certaines), mais elles n’ont pas beaucoup évolué depuis, ce qui fait qu’elles ne supportent pas des fonctionnalités introduites plus tard et devenues très courantes. En particulier, elles ne supportent pas l’attente asynchrone, ni l’annulation de l’attente. Heureusement, il est assez facile d’ajouter ces fonctionnalités via des méthodes d’extension.

Annulation

Commençons par le plus facile : l’annulation de l’attente. Dans certains cas, on voudrait pouvoir passer un CancellationToken à WaitHandle.WaitOne, mais aucune des surcharges ne le supporte. Notez que des variantes plus récentes de certaines primitives de synchronisation, comme SemaphoreSlim et ManualResetEventSlim, supportent l’annulation ; cependant elles ne sont pas appropriées dans toutes les situations, car elles sont conçues pour les cas où les temps d’attente sont très courts.

Un CancellationToken expose un WaitHandle, qui est signalé quand l’annulation est demandée. On peut tirer parti de cela pour implémenter l’attente asynchrone sur un wait handle :

public static bool WaitOne(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken)
{
    int n = WaitHandle.WaitAny(new[] { handle, cancellationToken.WaitHandle }, millisecondsTimeout);
    switch (n)
    {
        case WaitHandle.WaitTimeout:
            return false;
        case 0:
            return true;
        default:
            cancellationToken.ThrowIfCancellationRequested();
            return false; // never reached
    }
}

On utilise WaitHandle.WaitAny pour attendre que le wait handle d’origine ou celui du jeton d’annulation soit signalé. WaitAny renvoie l’index du premier wait handle qui a été signalé, ou  WaitHandle.WaitTimeout si un timeout s’est produit avant que l’un des wait handles ne soit signalé. On a donc 3 résultats possibles :

  • un timeout s’est produit : on renvoie false (comme la méthode WaitOne standard) ;
  • le wait handle d’origine a été signalé en premier : on renvoie true (comme la méthode WaitOne standard) ;
  • le wait handle du jeton d’annulation a été signalé en premier : on lance une OperationCancelledException.

Pour compléter, on peut rajouter quelques surcharges pour les cas d’utilisation courants :

public static bool WaitOne(this WaitHandle handle, TimeSpan timeout, CancellationToken cancellationToken)
{
    return handle.WaitOne((int)timeout.TotalMilliseconds, cancellationToken);
}
 
public static bool WaitOne(this WaitHandle handle, CancellationToken cancellationToken)
{
    return handle.WaitOne(Timeout.Infinite, cancellationToken);
}

Et voilà, on a maintenant une méthode WaitOne qui supporte l’annulation !

Attente asynchrone

Maintenant, comment faire pour attendre un wait handle de façon asynchrone ? C’est un peu plus difficile. Ce qu’on veut ici est une méthode WaitOneAsync qui va renvoyer un Task<bool> (et pendant qu’on y est, autant inclure aussi le support de l’annulation). L’approche habituelle pour créer un wrapper Task pour une opération asynchrone non basée sur une tâche est d’utiliser un TaskCompletionSource<T>, c’est donc ce qu’on va faire. Quand le wait handle sera signalé, on mettra à true le résultat de la tâche; si un timeout se produit, on le mettra à false; et si le jeton d’annulation est signalé, on marquera la tâche comme annulée.

J’ai eu un peu de mal à trouver un moyen d’exécuter un delegate quand un wait handle est signalé, mais j’ai fini par tomber sur la méthode ThreadPool.RegisterWaitForSingleObject, qui sert précisément à ça. Je ne sais pas trop pourquoi elle est dans la classe ThreadPool ; il me semble que ça aurait eu plus de sens de la mettre dans la classe WaitHandle, mais je suppose qu’il y a une bonne raison.

Voilà donc ce qu’on va faire :

  • créer un TaskCompletionSource<bool> ;
  • enregistrer un delegate qui mettra le résultat de la tâche à true quand le wait handle sera signalé, ou à false si un timeout se produit avant, à l’aide de ThreadPool.RegisterWaitForSingleObject ;
  • enregistrer un delegate pour marquer la tâche comme annulée quand le jeton d’annulation sera signalé, en utilisant CancellationToken.Register ;
  • désenregistrer les deux delegates une fois que la tâche est terminée.

Voici l’implémentation :

public static async Task<bool> WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken)
{
    RegisteredWaitHandle registeredHandle = null;
    CancellationTokenRegistration tokenRegistration = default(CancellationTokenRegistration);
    try
    {
        var tcs = new TaskCompletionSource<bool>();
        registeredHandle = ThreadPool.RegisterWaitForSingleObject(
            handle,
            (state, timedOut) => ((TaskCompletionSource<bool>)state).TrySetResult(!timedOut),
            tcs,
            millisecondsTimeout,
            true);
        tokenRegistration = cancellationToken.Register(
            state => ((TaskCompletionSource<bool>)state).TrySetCanceled(),
            tcs);
        return await tcs.Task;
    }
    finally
    {
        if (registeredHandle != null)
            registeredHandle.Unregister(null);
        tokenRegistration.Dispose();
    }
}
 
public static Task<bool> WaitOneAsync(this WaitHandle handle, TimeSpan timeout, CancellationToken cancellationToken)
{
    return handle.WaitOneAsync((int)timeout.TotalMilliseconds, cancellationToken);
}
 
public static Task<bool> WaitOneAsync(this WaitHandle handle, CancellationToken cancellationToken)
{
    return handle.WaitOneAsync(Timeout.Infinite, cancellationToken);
}

Notez que les expressions lambda auraient pu utiliser la variable tcs directement ; cela rendrait le code un peu plus lisible, mais causerait la création d’une closure, donc pour éviter cela et améliorer un peu les performances, on passe tcs via le paramètre state.

On peut maintenant utiliser notre méthode WaitOneAsync de la façon suivante :

var mre = new ManualResetEvent(false);
…
if (await mre.WaitOneAsync(2000, cancellationToken))
{
    …
}

Remarque importante : cette méthode ne fonctionnera pas pour un Mutex, car elle repose sur RegisterWaitForSingleObject, qui d’après la documentation ne fonctionne qu’avec les wait handles autres que Mutex.

Conclusion

On a donc vu qu’avec juste quelques méthodes d’extension, on peut rendre les primitives de synchronisation standard bien plus pratiques à utiliser dans du code moderne qui tire parti de l’asynchronisme et de l’annulation. Cependant je peux difficilement terminer ce billet sans mentionner la librairie AsyncEx de Stephen Cleary ; c’est une boite à outils très complète qui fournit des versions asynchrones de la plupart des primitives de synchronisation, dont certaines qui permettront d’arriver au même résultat que le code ci-dessus. Je vous invite à y jeter un œil, elle contient plein de choses utiles.

Test unitaires asynchrones avec NUnit

Récemment, mon équipe et moi avons commencé à écrire des tests unitaires pour une application qui utilise beaucoup de code asynchrone. Nous avons utilisé NUnit (2.6) parce que nous le connaissions déjà bien, mais nous ne l’avions encore jamais utilisé pour tester du code asynchrone.

Supposons que le système à tester soit cette très intéressante classe Calculator :

    public class Calculator
    {
        public async Task<int> AddAsync(int x, int y)
        {
            // simulate long calculation
            await Task.Delay(100).ConfigureAwait(false);
            // the answer to life, the universe and everything.
            return 42;
        }
    }

(Indice: ce code contient un bug… 42 n’est pas toujours la réponse. Ça m’a fait un choc quand j’ai appris ça!)

Et voici un test unitaire pour la méthode AddAsync :

        [Test]
        public async void AddAsync_Returns_The_Sum_Of_X_And_Y()
        {
            var calculator = new Calculator();
            int result = await calculator.AddAsync(1, 1);
            Assert.AreEqual(2, result);
        }

async void vs. async Task

Avant même de lancer ce test, je me suis dit : Ça ne va pas marcher! une méthode async void va retourner immédiatement sur le premier await, NUnit va donc croire que le test est terminé alors que l’assertion n’a pas été exécutée, et le test va donc passer même si l’assertion échoue. J’ai donc changé la signature de la méthode en async Task, en me croyant très malin d’avoir évité ce piège…

        [Test]
        public async Task AddAsync_Returns_The_Sum_Of_X_And_Y()

Comme prévu, le test a échoué, ce qui confirme que NUnit sait gérer les tests asynchrones. J’ai corrigé la classe Calculator, et je n’y ai plus pensé. Jusqu’au jour où j’ai remarqué qu’un collègue écrivait ses tests avec async void. J’ai donc commencé à lui expliquer pourquoi ça ne pouvait pas marcher, et j’ai essayé de le lui démontrer en ajoutant une assertion qui échouerait… et à ma grande surprise, le test a échoué, prouvant que j’avais tort !

Etant d’une nature curieuse, j’ai aussitôt commencé à investiguer… Ma première idée a été de vérifier le SynchronizationContext courant, et en effet, j’ai vu que NUnit l’avant remplacé par une instance de NUnit.Framework.AsyncSynchronizationContext. Cette classe maintient une file des continuations qui sont postées dessus. Après que la méthode async void retourne (c’est-à-dire la première fois qu’on await une tâche qui n’est pas encore terminée), NUnit appelle la méthode WaitForPendingOperationsToComplete, qui exécute toute les continuations de la file, jusqu’à ce que celle-ci soit vide. C’est seulement là que le test sera considéré comme terminé.

La morale de cette histoire est donc que vous pouvez écrire des tests async void avec NUnit 2.6. Cela fonctionne aussi avec les delegates passés à Assert.Throws, qui peuvent avoir le modificateur async. Cela étant dit, ce n’est pas parce que vous pouvez le faire que c’est forcément une bonne idée… Les frameworks de test unitaire n’ont pas tous le même support pour cela, et la prochaine version de NUnit (la 3.0, encore en alpha), ne supportera pas les tests async void.

Donc, à moins que vous ne comptiez rester sur NUnit 2.6.4 ad vitam æternam, il vaut probablement toujours utiliser async Task dans vos tests unitaires.

Passage de paramètres par référence à une méthode asynchrone

L’asynchronisme dans C# est une fonctionnalité géniale, et je l’ai beaucoup utilisé depuis son apparition. Mais il y a quelques limitations agaçantes; par exemple, on ne peut pas passer des paramètres par référence (ref ou out) à une méthode asynchrone. Il y a de bonnes raisons pour cela; la plus évidente est que si vous passez par référence une variable locale, elle est stockée sur la pile, or la pile ne va pas rester disponible pendant toute l’exécution de la méthode asynchone (seulement jusqu’au premier await), donc l’emplacement de la variable n’existera plus.

Cependant, cette limitation est assez facile à contourner : il suffit de créer une classe Ref<T> pour encapsuler la valeur, et de passer une instance de cette classe par valeur à la méthode asynchrone:

async void btnFilesStats_Click(object sender, EventArgs e)
{
    var count = new Ref<int>();
    var size = new Ref<ulong>();
    await GetFileStats(tbPath.Text, count, size);
    txtFileStats.Text = string.Format("{0} files ({1} bytes)", count, size);
}

async Task GetFileStats(string path, Ref<int> totalCount, Ref<ulong> totalSize)
{
    var folder = await StorageFolder.GetFolderFromPathAsync(path);
    foreach (var f in await folder.GetFilesAsync())
    {
        totalCount.Value += 1;
        var props = await f.GetBasicPropertiesAsync();
        totalSize.Value += props.Size;
    }
    foreach (var f in await folder.GetFoldersAsync())
    {
        await GetFilesCountAndSize(f, totalCount, totalSize);
    }
}

La class Ref<T> ressemble à ceci:

public class Ref<T>
{
    public Ref() { }
    public Ref(T value) { Value = value; }
    public T Value { get; set; }
    public override string ToString()
    {
        T value = Value;
        return value == null ? "" : value.ToString();
    }
    public static implicit operator T(Ref<T> r) { return r.Value; }
    public static implicit operator Ref<T>(T value) { return new Ref<T>(value); }
}

Comme vous pouvez le voir, il n’y a rien de très compliqué. Cette approche peut également être utilisée pour les blocs itérateurs (yield return), qui n’autorisent pas non plus les paramètres ref ou out. Elle a aussi un avantage par rapport aux paramètres ref et out standards: elle permet de rendre le paramètre optionel, par exemple si on n’est pas intéressé par le résultat (évidemment il faut que la méthode appelée gère ce cas de façon appropriée).

[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.