Le développement asynchrone en VB avec la TPL et la CTP Async Refresh

Téléchargez le tutorial et la source

Article par Eric Vernié Microsoft France Division Plate-forme & Ecosystème

Je rencontre les développeurs pour leur faire partager ma passion pour le développement autour des technologies .NET sans oublier le monde natif autour des Apis Win32.

J'ai également en charge la préparation de certains contenus pour des séminaires tel que les TechDays. Retrouvez moi sur les centres de développement Visual C++, Le Framework .NET, ainsi que le site sécurité.

Sommaire de l'article

  • Introduction
  • Pré requis
  • Solution 1 : Utilisation du modèle Event-Based Asynchronous Pattern (EAP)
  • Solution 2 : Utilisation des Tasks ou des Futures pour faire de l’asynchronisme
  • Solution 3 : Encapsuler le modèle EAP à l’aide de l’objet TaskCompletionSource
  • Solution 4 : Utilisation des nouvelles fonctionnalités async et await du modèle Task-based Asynchronous Pattern (TAP)
  • En résumé
  • Conclusion

Introduction

Définition de l’asynchronisme :
L'asynchronisme c'est pouvoir :

  • Exécuter une tâche T1, en arrière-plan mais sans interrompre le déroulement de la tâche principale TP.**
    Pour ce faire vous avez plusieurs outils à votre disposition. La classe Thread, le pool de thread, et désormais avec le Framework .NET 4.0, la classe Task.**
  • Vous souhaitez être notifié de l’état de la tâche T1. (Finie, Interrompue, en Erreur, en cours).**
    Il est donc important de fournir à la tâche T1, une ou des méthodes de rappels, qui serviront à notifier l'appelant.**
  • Vous souhaitez pouvoir arrêter si possible la tâche.**
    Il faut fournir un mécanisme d'arrêt simple, une variable d'état qui indique que l'arrêt est demandé. A savoir si l'arrêt est possible, ce qui n'est pas toujours le cas.**

Pour simplifier la mise en place de ce modèle, et pour homogénéiser les classes de la plate-forme .NET, Microsoft fournit plusieurs modèles :

  • Depuis sa création, nous avons le modèle Asynchronous Programing Model (APM) également connu comme le modèle Begin/End. De nombreuses classes dans la plateforme .NET l’utilisent, avec une méthode ayant comme préfix BeginXX qui planifie l'exécution asynchrone et qui retourne un IAsyncResultreprésentant l’état de l'opération asynchrone et qui prend comme paramètre - entre autre- une méthode de rappel AsyncCallback afin de notifier l'appelant. Lorsque l'opération est terminée, il faut appeler impérativement la méthode EndXX définie dans le modèle, afin de retrouver les informations si nécessaire et nettoyer les ressources. L'appel à cette méthode permet également de lever une exception qui aurait eu lieu dans le déroulement de la méthode asynchrone.
    Exemple la classe System.IO.FileStreamimplémentant les méthodes
    Public Overridable Function BeginRead ( _
    buffer As Byte(), _
    offset As Integer, _
    count As Integer, _
    callback As AsyncCallback, _
    state As Object _
    ) As IAsyncResult)

    Public Overridable Function EndRead ( _
    asyncResult As IAsyncResult _
    ) As Integer

  • Le modèle Event-based AsynchronousPattern (EAP), introduit avec la plateforme .NET 2.0, est défini par une méthode ayant le suffixe Async (ie MaMethodeAsync) qui démarre l'opération asynchrone. Lorsque l'exécution est finie, en erreur, ou interrompue, l'opération asynchrone lève un évènement nommée par convention avec le suffixe Completed (ie MonEventCompleted). Un délégué fournit la méthode de rappel qui est invoquée lorsque l'évènement est levé.

Exemple la classe System.Net.NetworkInformation.Ping implémente la méthode SendAsync, et l'évènement PingCompleted

La mise en place de ces deux modèles, sont totalement indépendant du mécanisme d'implémentation utilisé pour exécuter la requête asynchrone. Vous avez plusieurs solutions à votre disposition. La classe Thread, le pool de thread et désormais avec le Framework .NET 4.0, La classe Task. A l'avenir d’ailleurs, nous préconisons d'utiliser la classe Task, pour les raisons que je ne détails pas ici (pour plus d'infos, n'hésitez pas à visiter le blog https://blogs.msdn.com/b/devpara/)
Mais la classe Task, fournit, une simplicité d'utilisation, utilise efficacement les ressources des machines (Distribution automatique et dynamique des tâches sur les processeurs), améliore la performance et la réactivité des applications, facilite la lisibilité, l’écriture du code, et la gestion des exceptions.

En règle générale on utilise le modèle APM(Dans la plate-forme .NET en particulier) lorsqu'on développe dans les couches basses et quand on a besoin d’une grande flexibilité d’implémentation. Les développeurs d'application sont sans doute moins concerné par ce modèle, mais plus par le modèle EPM lorsque l'opération asynchrone a besoin d'un point de "Rendez-Vous".

Je ne rentre pas dans le détail d'implémentation de ces deux modèles, ce n'est pas exactement le sujet dans cet article, mais vous pourrez trouver tous les détails sur MSDN à ces adresses :
Asynchronous Programing Modelhttps://msdn.microsoft.com/fr-fr/library/ms228969(v=VS.85)
Event-Based Asynchronous Pattern : https://msdn.microsoft.com/fr-fr/library/hkasytyf(v=VS.85).aspx

Note : Bien évidement, vous n'êtes pas obligé d'adhérer à ces deux modèles pour effectuer des opérations asynchrones, mais ils ont le mérite d’unifier le développement sur la plate-forme .NET.

Le sujet que je souhaite aborder dans cet article concerne la manière d'enchainer et d'écrire plusieurs opérations asynchrones successives, sachant que l'opération n+1, dépend du résultat de l’opération n.
Comme sujet d'étude, nous allons reprendre l'exemple de mon précédant article, consacré à Microsoft Translator, ou nous avons vu comment effectuer une requête asynchrone pour traduire un texte d'un langage vers un autre, puis une requête asynchrone également pour synthétiser au format wave ce texte traduit.
Comme ces deux opérations étaient totalement dissociées cela n'a pas posé de problème notable, mais maintenant je corse la difficulté, car je souhaiterais que les deux opérations ce face l'une après l'autre, sachant que l'opération de synthèse dépendra du résultat de l'opération de traduction.
Comme ces deux opérations sont asynchrones, il y a de fortes chances que la requête de synthèse vocale s'exécute avant que la requête de traduction n'est finie. Problème donc !

Dans cet article, nous allons utiliser différentes techniques pour effectuer cette opération.

  1. Nous utiliserons le modèle EAP, et l'enchainement d'évènements.
  2. Toujours à l'aide du modèle EAP, nous verrons comment réécrire notre code afin qu'il soit "plus lisible", en utilisant les méthodes anonymes et les expressions lambda
  3. Nous aborderons l'objet TaskCompletionSource, afin d'encapsuler notre modèle EAP dans une Tâche.
  4. Et nous finirons par l’utilisation du nouveau modèle Task AsynchronousPatternsymbolisé par les nouveaux mots clés du langage VB Async et Await.

Pré requis

Pour réaliser les exemples de cet article il vous faut :

Solution 1 : Utilisation du modèle Event-Based Asynchronous Pattern (EAP)

Dans mon précédant article, j’ai créé une librairie en y incluant un proxy WCF, en lui demandant de générer des opérations asynchrones, comme illustré sur la figure suivante :

01

Visual Studio 2010, génère automatiquement tout la plomberie qui met en place le modèle Event-Based Asynchronous.
Nous y retrouvons en autre les méthodes TranslateAsync et SpeakAsync et leurs évènements associés TranslateCompleted et SpeakCompleted

Pour effectuer nos deux opérations asynchrones l’une unes après l’autre, voici la première solution.

  1. On s’abonne à l’évènement TranslatedCompleted
    Private _handlerTranslateCompleted As EventHandler(Of TranslateCompletedEventArgs)
    Me._handlerTranslateCompleted = New EventHandler(Of TranslateCompletedEventArgs)(AddressOf Me.Solution1_TranslateCompleted)
    AddHandler Me._clientTranslator.TranslateCompleted, Me._handlerTranslateCompleted

    Ici notre évènement est lié à la méthode de rappel Solution1_TranslateCompleted, qui sera exécutée lorsque l’opération de traduction sera achevée.

  2. Puis on exécute la requête de traduction.
    Me._clientTranslator.TranslateAsync(APPID, texte, de, vers)

  3. Dans la méthode Solution1_TranslateCompleted, on s’abonne à l’évènement SpeakCompleted
    Me._handlerSpeakCompleted = New EventHandler(Of SpeakCompletedEventArgs)(AddressOf Me.Solution1_SpeakCompleted)
    AddHandler Me._clientTranslator.SpeakCompleted, Me._handlerSpeakCompleted
    Ici notre évènement est lié à la méthode de rappel Solution1_SpeakCompleted qui sera exécutée lorsque l’opération de synthèse vocale sera achevée.

  4. Puis on exécute la requête de synthèse vocale.
    Me._clientTranslator.SpeakAsync(APPID, e.Result, Me._langageASynthetiser, "audio/wav")

  5. Enfin dans la méthode Solution1_SpeakCompleted, nous notifions le client que le résultat est disponible
    Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(e.Cancelled, e.Error, Me._userState1, e.Result))

Remarque : je ne détaille pas tout le code, vous le retrouverez en téléchargement avec cet article. Néanmoins voici le code de l’évènement qui permet de notifier le client
Public Delegate Sub TraduireEtSynthetiserHandler(Sender As Object, e As TraduireEtSynthetiserArgs)
Public Event TraduireEtSynthetiserCompleted As TraduireEtSynthetiserHandler

Private Sub SurTraduireEtSynthetiserCompleted(e As TraduireEtSynthetiserArgs)      
RaiseEvent TraduireEtSynthetiserCompleted(Me, e)
End Sub

Public Class TraduireEtSynthetiserArgs
Inherits EventArgs
' Methods
Public Sub New(ByVal canceled As Boolean, ByVal err As Exception, ByVal texte As String, ByVal wav As String)
Me.Canceled = canceled
Me.Error = err
Me.UserState1 = texte
Me.UserState2 = wav
End Sub
' Fields
Public Canceled As Boolean
Public [Error] As Exception
Public UserState1 As String
Public UserState2 As String
End Class

Note : Il bien évidement possible d’enchainer plus de deux opérations asynchrones, en utilisant le même modèle.

Cette solution fonctionne correctement, mais il possible de la rendre plus "lisible" à l'aide des méthodes anonymes et des expressions lambda.

En effet, au lieu d'instancier le gestionnaire d'évènement avec une méthode qui se trouve en dehors de la portée courante comme précédemment :

Me._handlerSpeakCompleted = New EventHandler(Of SpeakCompletedEventArgs)(AddressOf Me.Solution1_SpeakCompleted)
AddHandler Me._clientTranslator.SpeakCompleted, Me._handlerSpeakCompleted

On utilise la syntaxe des méthodes anonymes comme "sucre syntaxique", pour écrire la méthode dans le contexte courant, et le compilateur se charge du reste.

Dim handlerTranslateCompleted As EventHandler(Of TranslateCompletedEventArgs) = Nothing
handlerTranslateCompleted = Sub(Sender As Object, e As TranslateCompletedEventArgs)
End Sub

Listing complet de la méthode TraduireEtSynthetiser

Public Sub TraduireEtSynthetiserAsyncSolution1Bis(ByVal texte As String, ByVal de As String, ByVal vers As String)
Dim handlerTranslateCompleted As EventHandler(Of TranslateCompletedEventArgs) = Nothing
Dim handlerSpeakCompleted As EventHandler(Of SpeakCompletedEventArgs) = Nothing
handlerTranslateCompleted = Sub(Sender As Object, e As TranslateCompletedEventArgs)
RemoveHandler Me._clientTranslator.TranslateCompleted,
Me._handlerTranslateCompleted
If (Not e.Error Is Nothing) Then
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(e.Cancelled, e.Error, "", "Pas de synthèse puisque Erreur dans la traduction"))
ElseIf e.Cancelled Then
Me.SurTraduireEtSynthetiserCompleted(New
TraduireEtSynthetiserArgs(e.Cancelled, e.Error, "", "Opération de traduction annulée"))
Else
handlerSpeakCompleted = Sub(Sendera As Object,
args As SpeakCompletedEventArgs)
RemoveHandler Me._clientTranslator.SpeakCompleted,
Me.handlerSpeakCompleted
If (Not args.Error Is Nothing) Then
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(args.Cancelled, args.Error, "", "Erreur dans la synthèse"))
ElseIf args.Cancelled Then
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(args.Cancelled, args.Error, "", "Opération de synthse annulée"))
Else
Me.SurTraduireEtSynthetiserCompleted(New
TraduireEtSynthetiserArgs(args.Cancelled,
args.Error, e.Result, args.Result))
End If
End Sub
AddHandler Me._clientTranslator.SpeakCompleted,
handlerSpeakCompleted
Me._clientTranslator.SpeakAsync(APPID, e.Result, vers,
"audio/wav")
End If
End Sub
AddHandler Me._clientTranslator.TranslateCompleted, handlerTranslateCompleted
Me._clientTranslator.TranslateAsync(APPID, texte, de, vers)
End Sub

Solution 2 : Utilisation des Tasks ou des Futures pour faire de l’asynchronisme

Comme je le disais plus haut, en lieu et place de l’utilisation de la classe Thread ou de la classe ThreadPool, pour effectuer une opération asynchrone, il est désormais conseillé d’utiliser la classe Task, disponible à partir du Framework 4.0.
Dans notre exemple, à la place d’utiliser les opérations asynchrones du service Web TranslateAsync et SpeakAsync, nous pourrionsutiliser leurs versions synchrones Translate et Speak, encapsulées dans des tâches, comme illustré dans le listing suivant :

Public Sub TraduireEtSynthetiserAsyncSolution2(ByVal texte As String, ByVal de As String, ByVal vers As String)
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
Dim traduit As String = String.Empty
Dim synthese As String = String.Empty
Task.Factory.StartNew(Sub()
Dim t1 As Task = Task.Factory.StartNew(Sub()
traduit = Me._clientTranslator.Translate(APPID, texte, de,
vers)
End Sub)
t1.Wait()
If Not t1.IsFaulted Then
synthese = Me._clientTranslator.Speak(APPID, texte, vers,
"audio/wav")
End If
End Sub).ContinueWith(Sub(tp As Task)
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(tp.IsCanceled,
tp.Exception, traduit, synthese))
End Sub, uiScheduler)
End Sub

Nous démarrons une nouvelle tâche Task.Factory.StartNew() pour démarrer une nouvelle opération asynchrone, afin de ne pas bloquer l’interface utilisateur. A l’intérieur de cette tâche, vous remarquerez que nous enchainons nos opérations synchrones mais comme elles sont encapsulées dans des tâches, elles se font en asynchrone. Afin que les tâches s’enchainent les unes à la suite des autres, on utilise la méthode Wait() de la tâche T1 pour attendre que c’elle ci finisse. Lorsque la 1er tâche est finie, on test T1.IsFaulted pour savoir si une erreur est survenue, si non, on exécute la seconde opération.
Une tâche est différent d’un thread dans le sens que c’est une API riche que l’on peut interroger pour connaitre son état T1.IsFaulted pour savoir si une exception a été levée dans ce cas la propriété T1.Exceptionn’est pas nulle, pour savoir si une opération est terminée T1.IsCompleted ou a été annulée par l’utilisateur T1.IsCanceled.
Une fois la dernière tâche terminée, on utilise le modèle de continuation de tâche, en appelant la méthode ContinueWith(), de la tâche principale afin de notifier le client que les différentes données sont arrivées, en levant l’évènement TraduireEtSynthetiserCompleted lors de l’exécution de la méthode SurTraduireEtSynthetiser(). Néanmoins, lever un évènement dans un thread différent que le thread principal, peut poser des problèmes si dans la méthode de rappel, le client manipule des éléments de l’interface. Il est vraisemblable qu’une exception du type "Le Thread appelant ne peut pas accéder à cet objet parcequ’un autre thread en est propriétaire" soit levée.
L’astuce pour y remédier est de synchroniser la levée de l’évènement avec le thread principal. Nous pourrions capturer le context de synchronisation par défaut (SynchronisationContext), et développer toute la mécanique de synchronisation. En réalité, nous allons laisser à la classe Task le soin de le faire pour nous. Nous capturons le contexte de synchronisation courrant via le TaskScheduler
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
que nous passons en tant que paramètre à la méthode ContinueWith().
ContinueWith(Sub(tp As Task)
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(tp.IsCanceled,
tp.Exception, traduit, synthese))
End Sub, uiScheduler)

Une autre possibilité d’opérations asynchrones, consiste à utiliser la notion de « futures ». Cette notion se traduit par une opération asynchrone qui retourne une valeur, en un mot une fonction asynchrone, alors qu’une tâche est juste une opération asynchrone.
Le « future » est défini dans le framework.NET 4.0 par la signature suivante Task ou TResult défini le type de retour. Pour faire court c’est simplement une tâche qui retourne une valeur, mais à la place d’explicitement utiliser la méthode Wait() pour attendre la fin de l’opération, on attend directement le résultat de la fonction, comme illustré dans le listing suivant :

Public Sub TraduireEtSynthetiserAsyncSolution2(ByVal texte As String, ByVal de As String, ByVal vers As String)
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
Task.Factory.StartNew(Function()
Dim F1 As Task(Of Tuple(Of String, String)) = _
Task(Of Tuple(Of String, String)) _
.Factory.StartNew(Function()
Dim traduction As String =
_clientTranslator.Translate(APPID, texte, de, vers)
Return New Tuple(Of String, _
String)(traduction, String.Empty)
End Function).ContinueWith(Of Tuple(Of String,
String))(Function(antecedantFuture As Task(Of Tuple(Of String, String)))
Dim traduction = antecedantFuture.Result.Item1
Dim synthese = _clientTranslator.Speak(APPID, traduction, vers,
"audio/wav")
Return New Tuple(Of String, String)(traduction, synthese)
End Function)
'Antendre que le résultat de la synthèse arrive
Return F1.Result
End Function).ContinueWith(Sub(antecedantFuture As Task(Of Tuple(Of String,
String)))
Me.SurTraduireEtSynthetiserCompleted(New
TraduireEtSynthetiserArgs(antecedantFuture.IsCanceled, _
antecedantFuture.Exception, _
antecedantFuture.Result.Item1, _
antecedantFuture.Result.Item2))
End Sub, uiScheduler)
End Sub

Nous créons ici un future Task(Of Tuple(Of String, String)).Factory.StartNew () qui retourne un Tuple(Of String,String), qui contiendra le résultat de la traduction et de la synthèse. Puis nous démarrons un autre future F1, avec la même signature
Dim F1 As Task(Of Tuple(Of String, String)) = Task(Of Tuple(Of String, String))
.Factory.StartNew() qui retourne également dans un Tuple le résultat de la traduction. Une fois le résultat obtenu, nous continuons l’exécution (ContinueWith(Function(antecedanteFuture)) en passant comme paramètre le future lui-même (antecedanteFuture) qui fournira à l’opération suivante le résultat et attendra le cas échéant antecedanteFuture.Result.
On retrouve sensiblement la même structure de code qu’avec l’utilisation des Tasks, à l’exception du faite que dans notre exemple, la fonction asynchrone retourne un Task(Tuple(Of String,String)), l’instruction return F1.Result, est bloquée, tant que l’opération asynchrone n’est pas finie. Rien de miraculeux ici, la propriété Result fait appel en interne à la méthode Wait(), mais on retrouve un modèle de développement qui nous est plus familier : Appel de méthode, retour de valeur.
Vous allez me dire mais pourquoi tout ce code ? car après tout on utilise deux méthodes synchrones et du code comme illustré ci-dessous, résout parfaitement notre problème.

Public Sub TraduireEtSynthetiserAsyncSolution2(ByVal texte As String, ByVal de As String, ByVal vers As String)
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
Dim traduit As String = String.Empty
Dim synthese As String = String.Empty
Task.Factory.StartNew(Sub()
Task.Factory.StartNew(Sub()
traduit = Me._clientTranslator.Translate(APPID, texte, de, vers)
synthese = Me._clientTranslator.Speak(APPID, traduit, vers,
"audio/wav")
End Sub).Wait()
End Sub).ContinueWith(Sub(tp As Task)
Me.SurTraduireEtSynthetiserCompleted(New TraduireEtSynthetiserArgs(tp.IsCanceled, _
tp.Exception, _
traduit, _
synthese))
End Sub, uiScheduler)
End Sub

En réalité, ce que nous souhaitons réellement c’est pouvoir encapsuler le modèle EAP dans une tâche donc utiliser les opérations asynchrones de notre service Web , et le faite d’aborder en amont cette structure, vous aidera à comprendre plus facilement la suite.

Solution 3 : Encapsuler le modèle EAP à l’aide de l’objet TaskCompletionSource

Pour pouvoir encapsuler le modèle EAP, nous allons utiliser l’objet TaskCompletionSource(Of TResult). Cet objet est utile pour toutes les opérations d’entrées/sorties, comme celle liée au réseau dans notre cas. Il retourne un Task, et possède une propriété Task que nous pouvons interroger pour connaitre l’état de l’opération encours.
Pour utiliser le TaskCompletionSource il faut procéder comme suit :

  1. Déclarez un délégué EventHandler
    Dim handlerTranslateCompleted As EventHandler(Of TranslateCompletedEventArgs) = Nothing

  2. Instanciez l’objet TaskCompletionSource
    Dim tcs As New TaskCompletionSource(Of String)

  3. Associez le délégué à une méthode, ou plus précisément dans notre exemple à une méthode anonyme sous forme d’expression lambda qui utilise les méthodes TrySetException, TrySetCanceled, TrySetResult de l’objet TaskCompletionSource.
    handlerTranslateCompleted = Sub(Sender As Object, e As TranslateCompletedEventArgs)
    If Not IsNothing(e.Error) Then
    tcs.TrySetException(e.Error)
    ElseIf (e.Cancelled) Then
    tcs.TrySetCanceled()
    Else
    tcs.TrySetResult(e.Result)
    End If
    RemoveHandler Me._clientTranslator.TranslateCompleted,
    handlerTranslateCompleted
    End Sub

    L’objet TasksCompletionSource démarre automatiquement une tâche et agit comme un « future » qui retourne une chaine de caractère par l’intermédiaire d’une Task(Tuple(Of String,String)) et bloque tant que l’opération n’est pas terminée.

  4. Puis on s’abonne à l’évènement lui-même
    AddHandler _clientTranslator.TranslateCompleted,handlerTranslateCompleted

  5. On exécute la méthode asynchrone
    _clientTranslator.TranslateAsync(APPID, texte, de, vers)

  6. La tâche se met en attente avant de retourner le résultat à l’appelant.
    Try
    traduction = tcs.Task.Result
    Catch ex As AggregateException
    tcs.TrySetException(ex)
    End Try
    Return New Tuple(Of String, String)(traduction, String.Empty)
    La tâche est bloqué sur l’instruction tcs.Task.Result., si une erreur survient, nous renvoyons l’erreur pour en informer la tâche suivante.

  7. Pour la synthèse vocale la structure est la même comme illustré dans le listing complet suivant.

Public Sub TraduireEtSynthetiserAsyncSolution3(ByVal texte As String, ByVal de As String, ByVal vers As String)
Dim handlerTranslateCompleted As EventHandler(Of TranslateCompletedEventArgs) = Nothing
Dim handlerSpeakCompleted As EventHandler(Of SpeakCompletedEventArgs) = Nothing
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
Task.Factory.StartNew(Of Tuple(Of String, String))(Function()
Dim tcs As New TaskCompletionSource(Of String)
handlerTranslateCompleted = Sub(Sender As Object, e As TranslateCompletedEventArgs)
If Not IsNothing(e.Error) Then
tcs.TrySetException(e.Error)
ElseIf (e.Cancelled) Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
RemoveHandler Me._clientTranslator.TranslateCompleted,
handlerTranslateCompleted
End Sub

'je m'abonne à l'évènement
AddHandler _clientTranslator.TranslateCompleted, handlerTranslateCompleted
Try
_clientTranslator.TranslateAsync(APPID, texte, de, vers)
Catch ex As Exception
RemoveHandler Me._clientTranslator.TranslateCompleted,
handlerTranslateCompleted
End Try
Dim traduction As String = String.Empty
Try
traduction = tcs.Task.Result
Catch ex As AggregateException
tcs.TrySetException(ex)
End Try
Return New Tuple(Of String, String)(traduction, String.Empty)
End Function).ContinueWith(Of Tuple(Of String, String))(Function(antecedantFuture As Task(Of Tuple(Of String, String)))
Dim tcs As New TaskCompletionSource(Of String)
Dim traduction As String = String.Empty
Dim synthese As String = String.Empty
If (antecedantFuture.IsFaulted = False) Then
handlerSpeakCompleted = Sub(Sender As Object, e As
SpeakCompletedEventArgs)
If Not IsNothing(e.Error) Then
tcs.TrySetException(e.Error)
ElseIf (e.Cancelled) Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
RemoveHandler Me._clientTranslator.SpeakCompleted,
handlerSpeakCompleted
End Sub
AddHandler _clientTranslator.SpeakCompleted, handlerSpeakCompleted
traduction = antecedantFuture.Result.Item1
Try
_clientTranslator.SpeakAsync(APPID, traduction, vers, "audio/wav")
Catch ex As Exception
RemoveHandler Me._clientTranslator.SpeakCompleted,
handlerSpeakCompleted
End Try
synthese = tcs.Task.Result
End If
Return New Tuple(Of String, String)(traduction, synthese)
End Function).ContinueWith(Sub(antecedantFuture As Task(Of Tuple(Of String, String)))
If (antecedantFuture.IsFaulted) Then
Me.SurTraduireEtSynthetiserCompleted(New
TraduireEtSynthetiserArgs(antecedantFuture.IsCanceled, _
antecedantFuture.Exception, _
"", ""))
Else
Me.SurTraduireEtSynthetiserCompleted(New
TraduireEtSynthetiserArgs(antecedantFuture.IsCanceled, _
antecedantFuture.Exception, _
antecedantFuture.Result.Item1, _
antecedantFuture.Result.Item2))
End If

End Sub, uiScheduler)
End Sub

Néanmoins, je ne suis pas tout à fait satisfait de ce type de code qui reste encore un peu confus. En tant que développeur d’une librairie je préfère dissocier la partie traduction et synthèse vocale dans deux méthodes séparées, que nous nommerons TraduireAsync et SyntheseAsync et qui reprennent la même structure de code que précédemment, à l’exception qu’elles retournent toutes les deux un Task(OF String).

Public Function TraduireAsync(ByVal texte As String, ByVal de As String, ByVal vers As String) As Task(Of String)
Return Task.Factory.StartNew(Of String)(Function()
Dim handlerTranslateCompleted As EventHandler(Of
TranslateCompletedEventArgs) = Nothing
Dim tcs As New TaskCompletionSource(Of String)
handlerTranslateCompleted = Sub(Sender As Object, e As
TranslateCompletedEventArgs)
If Not IsNothing(e.Error) Then
tcs.TrySetException(e.Error)
ElseIf (e.Cancelled) Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
RemoveHandler Me._clientTranslator.TranslateCompleted,
handlerTranslateCompleted
End Sub
'je m'abonne à l'évènement
AddHandler _clientTranslator.TranslateCompleted, handlerTranslateCompleted
Try
_clientTranslator.TranslateAsync(APPID, texte, de, vers)
Catch ex As Exception
RemoveHandler Me._clientTranslator.TranslateCompleted,
handlerTranslateCompleted
End Try
Return tcs.Task.Result
End Function)
End Function
Public Function SpeakAsync(ByVal texte As String, ByVal langage As String) As Task(Of String)
Return Task.Factory.StartNew(Of String)(Function()
Dim tcs As New TaskCompletionSource(Of String)
Dim traduction As String = String.Empty
Dim handlerSpeakCompleted As EventHandler(Of SpeakCompletedEventArgs) = Nothing
handlerSpeakCompleted = Sub(Sender As Object, e As SpeakCompletedEventArgs)
If Not IsNothing(e.Error) Then
tcs.TrySetException(e.Error)
ElseIf (e.Cancelled) Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
RemoveHandler Me._clientTranslator.SpeakCompleted, handlerSpeakCompleted
End Sub
AddHandler _clientTranslator.SpeakCompleted, handlerSpeakCompleted
Try
_clientTranslator.SpeakAsync(APPID, texte, langage, "audio/wav")
Catch ex As Exception
RemoveHandler Me._clientTranslator.SpeakCompleted, handlerSpeakCompleted
End Try
Return tcs.Task.Result
End Function)
End Function

En agissant ainsi, on facilite le travail du client (le développeur qui développe l’interface graphique), peut écrire du code du type :

Private Sub cmdTraduireEtSynthetiser4_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles cmdTraduireEtSynthetiser4.Click
Dim textATraduire As String = Me.txtAtraduire.Text
Dim uiScheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext
Task.Factory.StartNew(Of Tuple(Of String, String))(Function()
Dim f1 As Task(Of String) =
Me._traducteur.TraduireAsync(textATraduire, "fr", "ko")
Dim traduction As String = f1.Result
Dim f2 As Task(Of String) =
Me._traducteur.SpeakAsync(traduction, "ko")
Dim synthese As String = String.Empty
If f1.IsFaulted = False Then
synthese = f2.Result
End If
Return New Tuple(Of String, String)(traduction, synthese)
End Function).ContinueWith(Sub(antecedant As Task(Of Tuple(Of String, String)))
If antecedant.IsFaulted Then
MessageBox.Show(antecedant.Exception.Message)
Else
Me.txtTraduit.Text = antecedant.Result.Item1
Me._snd.SoundLocation = antecedant.Result.Item2
Me._snd.LoadAsync()
End If
End Sub, uiScheduler)
End Sub

En utilisant les « futures », les opérations asynchrones, sont bloquées sur la valeur de retour comme vu précédemment et s’enchaine séquentiellement. Enfin de tâche, nous utilisons le modèle de continuation de tâche, avec un contexte de synchronisation par défaut, afin de mettre à jour des éléments de l’interface.
Néanmoins, même si nous avons simplifié l’écriture, il reste encore pas mal de travail pour le développeur de l’interface graphique. C’est là que rentre en jeu une nouvelle fonctionnalité encore à l’étude chez Microsoft, mais qui devrait vraisemblablement voir le jour avec la prochaine version de nos outils de développement.
Visual Studio Async CTP
Cette CTP propose de nouvelles fonctionnalités du langage VB ainsi qu’un nouveau modèle de développement pour l’asynchronisme, le modèle Task-based Asynchronous Pattern (TAP), afin de rendre la programmation asynchrone similaire à la programmation synchrone.

Solution 4 : Utilisation des nouvelles fonctionnalités async et await du modèle Task-based Asynchronous Pattern (TAP)

La beauté de la chose, c’est que, nous venons de voir comment mettre en place, le modèle TAP. En effet, ce modèle préconise qu’une méthode asynchrone, retourne soit une Task, soit Task, et c’est ce que font exactement nos deux méthodes TraduireAsync et SyntheseAsyncdans notre librairie, en retournant une Task, je ne reviendrai donc pas dessus.
Maintenant nous allons nous mettre dans la position d’un développeur de l’interface graphique et procéder comme suit :

  1. Il faut référencer l’assemblage AsyncCtpLibrary, comme illustré sur la figure suivante.
    02

    Cet assemblage ce trouve dans le répertoire C:\Users\[NOM UTILISATEUR]\Documents\Microsoft Visual Studio Async CTP\Samples

  2. Ensuite il faut marquer la méthode qui appellera nos opérations asynchrones avec le nouveau mot clé async du C# (apporté par l’assemblage AsyncCtpLibrary, sans lui le mot clé ne sera pas reconnu)
    private async void cmdTraduireEtSynthetiser5_Click(object sender, RoutedEventArgs e)
    Private Async Sub cmdTraduireEtSynthetiser5_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles cmdTraduireEtSynthetiser5.Click
    Vous noterez ici que j’ai marqué directement la méthode qui est déclenché par l’évènement click d’un bouton.

  3. Ensuite pour attendre l’exécution de chaque méthode asynchrone, il suffit d’utiliser le mot clé await.
    txtTraduit.Text = Await _traducteur.TraduireAsync(txtAtraduire.Text, "fr", "ko")
    snd.SoundLocation = Await _traducteur.SpeakAsync(txtTraduit.Text, "ko")

  4. Puis je charge en asynchrone de la même manière les données dans le SoundPlayer
    Await snd.LoadTaskAsync();

  5. Et je joue le son
    snd.Play();

Listing complet de l’exécution asynchrone

private Async Private Sub cmdTraduireEtSynthetiser5_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles cmdTraduireEtSynthetiser5.Click
Dim snd As New SoundPlayer
Try
txtTraduit.Text = Await _traducteur.TraduireAsync(txtAtraduire.Text, "fr", "ko")
snd.SoundLocation = Await _traducteur.SpeakAsync(txtTraduit.Text, "ko")
Await snd.LoadTaskAsync()
snd.Play()
Catch ex As AggregateException
If (ex.InnerExceptions.Count > 0) Then
MessageBox.Show(ex.InnerExceptions(0).Message)
Else
MessageBox.Show(ex.Message)
End If
End Try
End Sub

Vous remarquerez dans ce dernier listing :

  1. Que j’associe directement les valeurs de retour dans des objets graphiques, crées dans le thread principal. Plus besoin de se préoccuper du contexte de synchronisation, le modèle TAP s’en charge pour nous.
  2. Que l’enchainement des opérations asynchrones, se fait comme si nous développions en mode synchrone, même pour la gestion des exceptions.
  3. Que j’ai développé une méthode d’extension pour le SoundPlayer, prenant en charge le modèle TAP et dont voici le listing.

Public Module SoundPlayerEx

Public Function LoadTaskAsync(ByVal soundPlayer As SoundPlayer) As Task
If (soundPlayer Is Nothing) Then
Throw New ArgumentNullException("soundPlayer")
End If
Dim tcs As New TaskCompletionSource(Of Object)(soundPlayer)
Dim completedHandler As AsyncCompletedEventHandler = Nothing
completedHandler = Sub(sender As Object, e As AsyncCompletedEventArgs)
If (Not e.Error Is Nothing) Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(Nothing)
End If
RemoveHandler soundPlayer.LoadCompleted,
completedHandler
End Sub

AddHandler soundPlayer.LoadCompleted, completedHandler
soundPlayer.LoadAsync()
Return tcs.Task
End Function
End Module

A noter que pour faciliter notre travail, est livré avec l’async CTP refresh un certain nombre de méthodes d’extensions contenues dans la classe AsyncCtpExtensions. Donc avant de partir bille en tête et redévelopper sa propre extension, vérifiez qu’elle n’existe pas déjà ;-).
En utilisant ce modèle vous conviendrez que le code est beaucoup plus clair et se rapproche du code synchrone.

En résumé

Task-based Asynchronous Pattern est un nouveau modèle .NET pour les opérations asynchrones. Il est basé sur les Task et **Task(Of TResult)**utilisées pour des opérations asynchrones (Ce situe dans l’espace de nom System.Threading.Task).
Le début et la fin d’une opération basée sur ce modèle, sont représentés par l’appel d’une simpleméthode, par opposition aux deux précédents modèles Asynchronous Programing Model et Event-based Asynchronous Pattern.
Par convention, on nomme une méthode du modèle avec le suffixe Async. Cette méthode retourne un Task ou un Task(Of TResult),TResultreprésentant le type de retour, qui peut être un type simple, comme String, integer, long etc, ou des types complexes comme une structure voir un Tuple().

Il est important de comprendre, que la nouvelle fonctionnalité du langage Async, ne rend pas une méthode « asynchrone », c’est-à-dire, qu’elle n’est pas associée à un thread particulier. Il est impératif d’y introduire le mot clé Await, (Sans lui, le compilateur émet aujourd’hui une erreur relativement obscure du type Unable to load message string from resources.)
C’est seulement lorsque la méthode rencontre le premier await, qu’elle retourne à son appelant originel, ce qui signifie que dans ce type de méthode async, vous ne devez pas écrire beaucoup de code, ni faire d’appels bloquants avant le premier await ou entre plusieurs await. D’autre part si vous souhaitez ne pas bloquer le thread de l’interface graphique et exécuter une tâche qui prend du temps, il est explicitement nécessaire de pousser l’opération dans le pool de thread en utilisant, soit Task.Factory.StarNew si vous souhaitez un contrôle plus fin du TaskScheduler par exemple, ou alors les méthodes statiques prévues à cette effet dans la CTP Async Refresh TaskEx.Run et TaskEx.RunEx qui sont des raccourcis à la création de tâches.
Au niveau API, comme nous venons de le voir plus haut, pour attendre l’exécution d’une opération asynchrone, sans blocage, il faut utiliser des méthodes de rappels (des callbacks). Pour les tâches, il est possible de le faire à l’aide de méthodes comme ContinueWith. Avec le modèle TAP et la nouvelle fonctionnalité du langage C# await, le compilateur émet pour nous tous le code d’attente, et « cache » toutes les méthodes de rappels. Pour de plus amples informations sur les nouvelles fonctionnalités du langage C#, n’hésitez pas à télécharger l’article VB Language Specification for Asynchronous Functions

Conclusion

Aujourd’hui, développer des interfaces réactives, n’est plus une option - surtout avec le nombre de cœurs qui se multiplie sur nos PC -  et la programmation asynchrone peut aider dans ce sens. Mais les modèles actuels APM et EAP**,** même si ils sont connues et éprouvés sont sujet à de nombreuses erreurs de programmation. Le modèle TAP associé aux nouveaux éléments du langage Async et Await, facilite non seulement l’écriture du code asynchrone, mais également sa gestion des exceptions. Le but de TAP étant de rendre le développement asynchrone aussi prêt du paradigme de la programmation synchrone.
Pour ceux qui développent des librairies, proposer ce modèle, à ceux qui développent des applications, leur facilitera grandement la tâche !!
Pour en savoir plus sur le modèle TAP vous pouvez télécharger le document ici.