Maîtriser l’asynchronisme de C# 5.0 - Part 2
L’asynchronisme Microsoft avant C# 5.0
Pourquoi Microsoft a-t-elle introduit les deux identifiants async/await en C# 5.0 ? Avant de répondre à cette question, je vous propose un retour en C# 1.0, puis en C# 2.0, car bien avant C# 5.0, Microsoft a offert plusieurs solutions qui permettaient de programmer en asynchrone.
Comprendre l’offre asynchrone avant C# 5.0
L’objectif est d’analyser les éléments les plus saillants de chaque solution, afin de comprendre pourquoi Microsoft a décliné une nouvelle offre avec Visual Studio 2012. Enfin, je pense que cette rétrospective n’est pas négligeable pour comprendre en profondeur la proposition asynchrone de C# 5.0 que nous détaillerons dans le prochain article.
Lecture d’un fichier en mode synchrone
Présentation
Pour mieux comprendre les prochains exemples asynchrones, je vous propose un exemple reposant sur la classe FileStream. Vous pouvez utiliser cette classe pour lire/écrire dans un fichier en mode synchrone, via les méthodes Read et Write. Voici l’illustration de la méthode Read.
readonly byte[] _buffer = new byte[100];
public void ReadSync(string filename)
{
using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read,
FileShare.Read, 1024))
{
try
{
Int32 bytesRead = fs.Read(_buffer, 0, _buffer.Length);
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(_buffer, 0, bytesRead));
}
catch (IOException exception)
{
Console.WriteLine("Error : {0}", exception.Message);
throw;
}
}
}
La méthode ReadSync, instancie un objet de type FileStream, en passant en paramètres : le nom du fichier à ouvrir, le mode d’accès FileAccess.Read, le mode de partage en lecture via FilleShare.Read, et la taille du buffer où les octets lus seront stockés. Puis nous appelons la méthode Read sur l’objet FileStream, où nous passons le buffer d’accueil des octets, l’offset de départ de la lecture à zéro et enfin, le nombre d’octets que nous souhaitons lire.
Comprendre l’implémentation
Reprenons l’exemple précédant à partir de l’appel à la méthode Read. Lorsque vous appelez cette méthode, votre code effectue une transition depuis votre thread managed vers du code natif Win32 via la méthode ReadFile. ReadFile alloue une petite structure appelée I/O Request Packet (IRP). Cette structure est initialisée afin de contenir principalement : le descripteur du fichier, l’offset du début de lecture, l’adresse mémoire du buffer de bytes où la lecture débutera et qui devront être remplis par les octets lus, et le nombre d’octets à transférer.
L’implémentation de fonctions Win32 ReadFile repose sur la méthode NtReadFile, qui se trouve dans la partie noyau de Windows. L’appel à la fonction noyau NtReadFile prend en paramètre la structure IRP qui marque une transition en mode noyau. À partir du descripteur de fichiers contenu dans la structure IRP, le noyau détermine le driver sous-jacent. Ainsi, Windows peut poster la structure IRP dans la queue de type IRP du driver NTFS sous-jacent. En fait, chaque driver maintient une queue IRP afin de répondre aux besoins de tous les processus en cours d’exécution sous Windows. Lorsque notre structure IRP est dépilée, elle est appliquée au matériel respectif, dans notre cas, le matériel est un disque dur. C’est alors que le disque dur va réaliser l’opération de type Entrée/Sortie (E/S) réclamée : lecture de 100 octets (la taille choisie est complètement arbitraire).
Pendant que le périphérique exécute son opération de type E/S, votre thread qui est à l’origine de l’appel n’a rien à faire. C’est pour cette raison que Windows place en sommeil votre thread afin de ne pas gaspiller du temps CPU pour rien. Cependant, de nombreuses structures mémoires ont été allouées. Par exemple, la pile en mode utilisateur, la pile en mode noyau et d’autres structures mémoires qui sont présentes et totalement inaccessibles. De plus, si l’application possède une interface graphique, la couche graphique ne peut plus répondre à l’utilisateur tant que le thread est bloqué.
Lorsque le périphérique aura terminé son opération de type E/S, alors Windows réveillera le thread appelant afin de l’exécuter et de retourner depuis le mode noyau vers le mode utilisateur. Depuis le mode utilisateur, le code va retourner dans le thread managed initial, au niveau du retour de la méthode Read. Cette méthode peut alors retourner le nombre d’octets lus : bytesRead. À ce stade, le code peut consulter les octets du buffer dans la limite de la variable byteRead.
Conclusion
Comme nous l’avons expliqué dans la première partie, les appels synchrones ne sont pas gênants, s’ils ne nuisent pas aux utilisateurs ou à la disponibilité des services. Notre exemple à l’avantage d’être à la fois simple et facile à comprendre. Cependant si le fichier se trouve sur un média particulièrement lent avec un nombre d’octets beaucoup plus conséquent, nous pourrions nous retrouver avec des latences insupportables pour un utilisateur. Pour éviter ces désagréments, nous pourrions rendre ce code asynchrone. Dans les exemples suivants, nous étudierons les techniques asynchrones relatives à la méthode Read à travers différentes API afin de juger à la fois le code engendré et l’implémentation interne.
Asynchronisme en C# 1.0
Présentation
Vous avez peut-être déjà utilisé les API asynchrones disponibles depuis C# 1.0. Ce sont les versions asynchrones de méthodes synchrones relatives à des entrées/sorties (Système de fichier, Réseau, Web …). Sur le plan de la syntaxe, le principe est simple, une méthode synchrone nommée XXX() sera déclinée sous la forme d’un couple de méthodes de type BeginXXX/ EndXXX. La communication entre les deux méthodes est assurée par l’interface IAsyncResult. On appelle aussi ce modèle APM, pour Asynchronous Programming Model.
Readonly byte[] _buffer = new byte[100];
public void ReadFileWithApm(string filename)
{
var fs = new FileStream(filename, FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
fs.BeginRead(_buffer, 0, _buffer.Length, ReadIsDone, fs);
}
private void ReadIsDone(IAsyncResult ar)
{
var fs = (FileStream)ar.AsyncState;
try
{
Int32 bytesRead = fs.EndRead(ar);
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(_buffer, 0, bytesRead));
}
finally
{
fs.Close();
}
}
La lecture est déférée au niveau du driver NTFS qui réceptionne cette demande de lecture d’octets dont la taille est Buffer.Length. Noter que l’option FileOptions doit utiliser le drapeau FileOptions.Asynchronous pour que l’exécution se déroule vraiment en mode asynchrone. Dans notre cas nous souhaitons lire quelques octets. Dans ce modèle, il n’y a pas de création de thread de travail supplémentaire. Sur le plan de l’exécution, ce modèle est parfait, nous sommes ici dans une implémentation 100% asynchrone.
Communication asynchrone
Pour bien comprendre chacune des offres, nous étudierons la structure de communication asynchrone. Comme nous l’avons déjà dit, c’est l’interface IAsyncResult qui assure la communication entre les méthodes BeginRead et EndRead:
public interface IAsyncResult
{
bool IsCompleted { get; }
WaitHandle AsyncWaitHandle { get; }
object AsyncState { get; }
bool CompletedSynchronously { get; }
}
La propriété IsCompleted permet de savoir si le traitement asynchrone est terminé. Naturellement, cette propriété pourrait être utilisée au sein d’une boucle de manière à vérifier si le traitement est terminé. Mais cette philosophie est totalement opposée avec la volonté de rendre votre code le plus disponible, car dans les faits cette boucle ne fait que gâcher du temps CPU.
La propriété AsyncWaitHandle, permet de récupérer une instance d’un type WaitHandle permettant d’attendre la fin du traitement de manière synchrone. Encore une fois, cette technique annule complètement le bénéfice attendu vis-à-vis de l’utilisateur. Pour information, la méthode WaitOne permet aussi de fournir un délai d’attente maximum. Le fragment de code ci-dessous, illustre l’utilisation de la méthode WaitOne avec un délai d’attente.
if (ar.AsyncWaitHandle.WaitOne(2000, true))
{
Int32 bytesRead = fs.EndRead(ar);
fs.Close();
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(Buffer, 0, bytesRead));
}
Si le résultat arrive avant 2 secondes le traitement se terminera correctement, sinon vous considérez que le traitement a échoué. Cependant, ce type de code ne devrait jamais être utilisé en production où la latence doit être la plus brève possible.
La propriété AsyncState permet de véhiculer un type personnalisé depuis la méthode appelante. Dans notre exemple, nous avons passé une instance de type FileStream :
fs.BeginRead(Buffer, 0, Buffer.Length, ReadIsDone, fs);
Que nous récupérons dans la méthode ReadIsDone.
private static void ReadIsDone(IAsyncResult ar)
{
var fs = (FileStream)ar.AsyncState;
La propriété CompletedSynchronously permet de savoir si l’opération en mode asynchrone, s’est terminée de manière synchrone. C’est encore une information que nous pourrions exploiter dans le cadre d’une boucle, mais comme nous l’avons déjà dit dans le cadre de la propriété IsCompleted, cette propriété n’offre que peu d’intérêt vis-à-vis d’une exécution asynchrone.
Dans ce premier exemple, nous avons négligé la gestion des exceptions. Voici une version modernisée contenant un traitement des exceptions et un remplacement de la méthode ReadIsDone par une expression lambda, afin d’obtenir un code plus concis.
readonly byte[] _buffer = new byte[100];
void ReadFileWithApmAndLambda(string filename)
{
var fs = new FileStream(filename, FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
fs.BeginRead(_buffer, 0, _buffer.Length, ar =>
{
try
{
Int32 bytesRead = fs.EndRead(ar);
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(_buffer, 0, bytesRead));
}
catch (IOException exception)
{
Console.WriteLine("Error : {0}", exception.Message);
throw;
}
finally
{
fs.Close();
}
}, null);
}
Cette version est à la fois plus complète, mais reste améliorable. Par exemple si notre code devait tourner dans une application graphique, nous aurions dû placer un code de synchronisation pour mettre à jour les contrôles graphiques depuis l’expression lambda qui s’exécute dans un thread du pool.
Comprendre l’exécution
Pour lire notre fichier, nous avons utilisé successivement les méthodes ReadFileWithApm et ReadFileWithApmAndLambda. Sur le plan de l’exécution, les deux programmes sont très similaires. Dans les deux cas, nous construisons un objet FileStream, mais cette fois nous passons le drapeau FileOptions.Asynchronous, afin de dire à Windows que nous souhaitons lire et écrire via des opérations asynchrones.
Pour lire le fichier, nous utilisons cette fois l’appel BeginRead à la place de Read. Comme Read, BeginRead appelle la fonction Win32 ReadFile. ReadFile alloue et initialise une structure IRP et passe celle-ci à la méthode noyau NtReadFile qui la poste dans la queue IRP du driver déduit du descripteur de ficher. Contrairement au scénario synchrone où le thread appelant était bloqué, il retourne immédiatement vers l’appel BeginRead. Mais cette fois nous avons passé une continuation : la méthode de type callback, ReadIsDone dans le premier programme et l’expression lambda dans le second programme. Un délégué dans la structure IRP pointe sur la continuation de manière à l’exécuter lorsque le driver aura effectué la totalité de l’opération. Donc, quelque part dans le futur, le pool de threads va extraire l’IRP et invoquer votre continuation. Le prototype de la continuation ne retourne rien et prend un paramètre de type IAsyncResult. Cette interface permet d’informer la continuation sur l’état de l’opération asynchrone comme nous l’avons décrit dans la partie « Communication asynchrone ».
Conclusion
Pourquoi l’offre APM n’a-t-elle pas rencontré un franc succès auprès des développeurs (alors que celle-ci repose sur une implémentation 100 % asynchrone) ? Le problème principal est facile à comprendre, il suffit d’observer le code. La méthode synchrone Read est séparée en deux méthodes BeginRead et EndRead, ce qui ne permet pas de lire et de maintenir le code facilement. À chaque appel de la méthode BeginRead une référence sur l’interface IAsyncResult est retournée. Celle-ci doit être lue une seule fois par la méthode EndRead. Dans le cadre d’un code plus complexe, avec plusieurs appels à BeginRead, le développeur devra s’assurer d’une parfaite relation avec les appels EndRead respectifs. Ceci peut être considéré comme une difficulté de programmation pour de nombreux développeurs. L’interface de communication IAsyncResult est rudimentaire et contient quelques propriétés peu pertinentes au regard de l’asynchronisme. Enfin, la méthode EndRead s’exécute dans un thread du pool de threads ce qui peut occasionner des problèmes de concurrences vis-à-vis de la mise à jour de contrôles graphiques. Cependant, les développeurs d’applications graphiques comme Windows Forms et WPF, connaissent généralement bien la classe SynchronizationContext permettant d’envoyer ou de poster dans le thread graphique la portion de code permettant une mise à jour de contrôles graphiques.
Event-Based Asynchronous Pattern (EAP)
Présentation
En C# 2.0, une nouvelle offre asynchrone orientée évènement fut introduite sous le nom de EAP pour Event-Based Asynchronous Pattern. L’objectif de cette offre est de signaler la fin du traitement asynchrone sur la base d’un évènement. Cette solution est particulièrement adaptée aux applications graphiques qui par nature sont orientées évènements. Cette offre est parfois utilisée via la surface de design avec certains contrôles Windows Forms. Pour illustrer ce modèle, nous étudierons la classe WebClient. Nous utiliserons la méthode DownloadStringAsync qui permet d’initialiser un téléchargement asynchrone (la version synchrone se nomme DownloadString). Dans le cas d’un téléchargement asynchrone, la WebClient expose un évènement DownloadStringCompleted qui est levé lorsque le chargement de la page WEB est terminé. Le code suivant illustre l’usage du pattern avec la classe WebClient :
var wc = new WebClient();
wc.DownloadStringCompleted += (o, e) =>
{
var html = e.Result;
listView.Items.Add(new PageLength(uri, uri.Length));
txtResult.Text = "LENGTH: " + html.Length.ToString("N0");
};
wc.DownloadStringAsync(new Uri("https://blogs.msdn.com/b/nativeconcurrency/"));
Une surcharge à la méthode DownloadStringAsync permet de passer un jeton permettant d'identifier l’appelant au niveau de l’évènement, autorisant des scénarios où de multiples appels simultanés sont nécessaires :
public void DownloadStringAsync (Uri address, object userToken);
Le code est relativement simple à écrire en comparaison avec le modèle APM. À l’instar de l’offre APM l’exécution interne reste quasi identique. On requête une URL Web qui a pour conséquence d’enregistrer cette demande du côté du driver HTTP, puis de retourner vers l’appelant en ayant pris soin d’édifier un lien entre la méthode de type callback via le pool de thread E/S et le driver HTTP. Lorsque le driver recevra la totalité de la page Web, le contenu sera copié dans la propriété Result de la classe DownloadStringCompletedEventArgs que nous détaillons plus loin. L’exécution de l’évènement DownloadStringCompletedEventHandler, a lieu dans le thread de l’appelant, ce qui signifie qu’un changement de contexte a été fait. En d’autres mots, l’offre EAP synchronise le code de l’évènement avec le thread appelant, souvent le thread graphique.
Communication asynchrone
Contrairement au modèle APM où la communication est assurée par une structure rudimentaire IAsyncResult, l’évènement EAP possède un argument bien plus riche fonctionnellement, DownloadStringCompletedEventArgs. Cette classe dérive de la classe AsyncCompletedEventArgs pour y ajouter la propriété Result, que nous avons déjà commentée.
public class DownloadStringCompletedEventArgs : AsyncCompletedEventArgs
{
public string Result { get { … } }
…
}
L’essentiel des propriétés se trouve dans la classe mère AsyncCompletedEventArgs.
public class AsyncCompletedEventArgs : EventArgs
{
…
public bool Cancelled { get { … } }
public Exception Error { get { … } }
public object UserState { get { … } }
}
- La propriété Cancelled permet de savoir si le traitement asynchrone a été abandonné (la méthode wc.CancelAsync a été appelée).
- La propriété Error permet de récupérer potentiellement une exception levée durant le traitement.
- La propriété UserState permet de véhiculer un type personnalisé.
Dans ce premier exemple, nous avons négligé la gestion des exceptions. Voici une version modernisée contenant l'utilisation d'une expression lambda, un traitement des exceptions, et l’illustration du traitement d’annulation.
var wc = new WebClient();
wc.DownloadStringCompleted += (o, e) =>
{
if (e.Error != null)
{
try
{
// restore strusctured exception handling
throw e.Error;
}
catch (WebException ex)
{
// Handle an exception
}
}
if (e.Cancelled) return;
var html = e.Result;
totalLength += html.Length;
listView.Items.Add(new PageLength(uri, uri.Length));
txtResult.Text = "TOTAL LENGTH: " + totalLength.ToString("N0");
};
wc.DownloadStringAsync(new Uri(uri));
wc.CancelAsync();
Conclusion
Dans le cadre d’un développement graphique, l’offre EAP est supérieure au modèle APM à la fois sur le plan de l’API, et sur le plan de la structure de communication asynchrone. La structure de communication est plus pertinente vis-à-vis d’un usage asynchrone. La capacité d’annuler le traitement est prise en compte, ce qui n’est pas le cas du modèle APM (même si vous pouvez écrire votre propre gestion d'annulation de traitement). Cette offre est conçue pour exécuter son évènement de retour d’exécution dans le contexte de l’interface graphique, donc contrairement au modèle APM, l’expression lambda s’exécute dans le thread de l’appelant. Vous avez sans doute remarqué que la définition de l’évènement consacré au traitement du résultat asynchrone est spécifiée avant d'initialiser le traitement lui-même, ce qui sur le plan de l’usage est un peu déroutant.
Task-Based Asynchronous Pattern (TAP)
Le type Task a été introduit avec la version 4.0 du Framework .NET afin d’offrir une forme de remplacement à l’API Thread jugé complexe et coûteuse en ressource (https://blogs.msdn.com/b/devpara/archive/2010/04/06/programmation-parall-le-avec-c-4-0-offre-parall-le-orient-e-t-ches-part-1.aspx). Le type Task se décline aussi sous la forme d’un type générique symbolisant un traitement dont le résultat sera disponible dans le futur.
Communication asynchrone
Si vous revenez un instant sur les modèles APM et EAP, nous avons dans les deux modèles une forme de résultat spécifique. Dans le premier cas, le résultat est incarné par l’interface IAsyncResult et dans le second cas, le résultat est représenté par la classe DownloadStringCompletedEventArgs.
La classe Task<TResult> résout l’essentiel des complexités rencontrées dans les modèles précédents. Dans ce cadre, les éléments les plus intéressants sont :
· La propriété Result permet de récupérer le résultat du traitement. Notons que la propriété Result est synchrone et donc bloquante, tant que le traitement asynchrone n’est pas terminé.
· La propriété Exception permet de récupérer potentiellement une exception levée durant le traitement.
· La méthode ContinueWith permet de lancer une nouvelle tâche dès que la première sera terminée, c’est une continuation de la première tâche.
Avec le Framework 4.5, nous disposons d’une nouvelle déclinaison de méthodes asynchrones. Par convention ces nouvelles méthodes sont suffixées par le mot Async. Dans notre exemple, la classe WebClient contenait déjà une méthode dont la signature se terminait par Async : DownloadStringAsync. Nous savons que cette méthode joue le rôle de démarreur du traitement asynchrone dans le modèle EAP. La nouvelle version du Framework 4.5 expose une nouvelle méthode, nommée DownloadStringTaskAsync et qui retourne un type Task<string>. Ces nouvelles méthodes asynchrones représentent un nouveau modèle appelé Task-Based Asynchronous Pattern (TAP). Ce nouveau modèle repose sur l’utilisation du type Task<TResult> pour toutes les méthodes post fixées par Async. Ainsi l’appelant peut poursuivre l’exécution à travers une continuation de l’API Task. Voici l’illustration de la version TAP de notre exemple avec la classe WebClient.
Task<string> task = new WebClient().DownloadStringTaskAsync(uri);
task.ContinueWith(previous =>
{
if(previous.Exception == null)
{
_totalLength += previous.Result.Length;
listView.Items.Add(new PageLength(uri, uri.Length));
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Dans cet exemple, nous utilisons la nouvelle méthode DownloadStringTaskAsync pour démarrer le téléchargement d’une page web. Dans la continuation, ContinueWith, nous utilisons une tâche afin de récupérer le résultat et ajouter au contrôle graphique les informations requises. Pour satisfaire la contrainte de mise à jour d’un contrôle graphique depuis un thread différent du thread graphique, nous exécutons toute la tâche de continuation dans le thread graphique en plaçant dans le paramètre dédié au Scheduler via TaskScheduler.FromCurrentSynchronizationContext() .
Sur le plan de l’exécution, le modèle TAP est très proche du modèle APM, lorsque les méthodes appelées sont relatives à des opérations de type E/S. L’appel de la méthode DowloadStringTaskAsync engendre une suite d’appels qui aboutira par poster une structure IRP au sein du driver HTTP. Mais à la différence du modèle APM, le retour immédiat vers l’appelant va retourner une instance du type Task<string> . Cet objet permet d’appeler la méthode ContinueWith pour enregistrer une méthode de type callback qui sera appelée lorsque l’opération de type E/S sous-jacente à DownloadStringTaskAsync sera terminée. Lorsque le téléchargement de la page Web sera terminé, une structure IRP initialisée avec les informations relatives au téléchargement sera placée dans l’un des threads du pool de thread E/S. Si une tâche de continuation a été enregistrée, celle-ci sera initialisée et exécutée par le thread du pool sélectionné.
Implémenter une méthode TAP
Dans les paragraphes précédents, nous avons constaté que l’utilisation des méthodes TAP est relativement simple. Cependant, nous n’avons pas abordé l’implémentation. Pour illustrer ce sujet, je vous propose la réécriture d’une nouvelle méthode du type Task du Framework 4.5 :
public static Task Delay(int millisecondsDelay)
Cette méthode permet de créer une tâche qui se terminera après un délai fourni par l’utilisateur.
Pour implémenter cette méthode, il nous faut faire appel à un type déjà présent dans le Framework 4.0 :
TaskCompletionSource<TResult>
Le type de TaskCompletionSource<TResult> sert à deux objectifs connexes, les deux font allusion à son nom: il est une source de création d'une tâche, et la source de l'achèvement de cette tâche. Généralement, vous créez une instance de TaskCompletionSource<TResult> puis vous gérez à la main ce que sera la tâche sous-jacente. Cette tâche est accessible via sa propriété Task. Contrairement aux tâches créées par Task.Factory.StartNew, une instance de TaskCompletionSource<TResult> n’est pas animée par un scheduler. Au contraire, le type TaskCompletionSource<TResult> fournit des propriétés et des méthodes qui vous permettent de contrôler la vie et l'achèvement de la tâche associée en fonction de vos besoins. Le type expose les méthodes suivantes : SetResult, SetException, SetCanceled, ainsi que TrySet*, permettant de gérer complètement à la main la vie de la tâche sous-jacente. Cependant, certaines situations peuvent donner lieu à des exécutions concurrentes où plusieurs threads peuvent tenter d'accéder à la source d'achèvement simultanément. Dans ce cas les déclinaisons des méthodes TrySet* retournent des booléens indiquant le succès plutôt que de jeter une exception.
Voici une implémentation simpliste, mais opérationnelle de la méthode Delay, illustrant l’usage du type de TaskCompletionSource<TResult>.
public Task Delay(int milliseconds)
{
var tcs = new TaskCompletionSource<object>();
var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
timer.Elapsed += delegate
{
timer.Dispose();
tcs.SetResult(null);
};
timer.Start();
return tcs.Task;
}
static void Main()
{
var prog = new Program();
prog.Delay(5 * 1000).ContinueWith((p) =>
Console.WriteLine("After five seconds"));
Console.Read();
}
Dans un premier temps, nous créons une instance de type TaskCompletionSource<object> car la méthode Delay retourne un type Task non générique. Puis nous créons une instance de type Timer en passant en paramètre le nombre de millisecondes à attendre, tout en indiquant que ce timer ne s’exécutera qu’une seule fois via la propriété AutoReset placée à false. Puis nous nous abonnons à l’évènement Elapsed, où nous fermons les ressources du timer, puis au niveau de l’instance TaskCompletionSource nous indiquons que la tâche ne retournera pas de valeur. Nous lançons alors le timer. Enfin nous retournons la valeur de la propriété Task de l’instance TaskCompletionSource l’instance.
L’objectif de ce paragraphe est de vous sensibiliser sur l’implémentation d’une méthode asynchrone. L’utilisation type TaskCompletionSource est largement utilisée dans l’implémentation de méthodes asynchrones.
Conclusion
Le modèle TAP s’appuie pleinement sur un jeu de nouvelles méthodes asynchrones disponibles à la fois en .NET 4.5 et sur WinRT. Le modèle s’appuie sur le type Task générique pour fournir une solution bien plus simple à utiliser que les modèles APM et EAP. Sur le plan de l’exécution les nouvelles méthodes asynchrones E/S du Framework 4.5 reposent sur un modèle interne 100 % asynchrone comme pour les modèles précédents. Sur le plan de l’utilisation, le modèle TAP est bien plus simple que les modèles EAP et APM. À la fois plus naturel dans l’expression et plus souple dans l’usage, le modèle TAP n’est pas satisfaisant lorsque nous l’utilisons à travers du code fortement impératif comme au travers de boucles par exemple, comme nous l’avons démontré dans la première partie.
Utiliser TPL pour appeler le pattern APM
Depuis le Framework .NET 4.0, la librairie TPL simplifie grandement les appels asynchrones pour des méthodes de longue durée. Cependant, il est généralement préférable d’utiliser les méthodes APM que d’utiliser directement TPL pour implémenter la version asynchrone d’un code orienté E/S. Heureusement, la classe statique TaskFactory de la class Task, a été pensé pour invoquer le couple de méthodes APM. Voici une illustration de l’exemple précédent décliné avec la méthode FromAsync.
var fs = new FileStream(filename, FileMode.Open, FileAccess.Read,
FileShare.Read, 1024, FileOptions.Asynchronous);
Task<int> task = Task<int>.Factory.FromAsync(
fs.BeginRead, fs.EndRead, Buffer, 0, Buffer.Length, null);
task.ContinueWith(previous =>
{
fs.Close();
if (!previous.IsFaulted)
{
Int32 bytesRead = previous.Result;
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(Buffer, 0, bytesRead));
}
else
{
// Handle the exception
}
});
Le type Task implémente l’interface IAsyncResult, ainsi les tâches peuvent supporter le modèle APM et parfois les étendre. Par exemple, la propriété AsyncState du type Task étend la propriété AsyncState de l’interface IAsyncResult, ce qui vous permet de transmettre un type personnalisé en interne du traitement asynchrone. Car vous l’aurez compris, la méthode EndRead est appelée en interne, afin d’obtenir un code plus simple, orienté TAP. Un code orienté TAP est parfait pour être utilisé avec l’offre async/await que nous étudierons dans la prochaine partie.
Conclusion
Dans cette seconde partie, nous avons montré à travers une rétrospective des technologies asynchrones Microsoft, les principaux défauts de ces offres : de la plus archaïque (APM) en passant par la plus évènementielle (EAP) et terminant avec la plus simple (TAP). Même si le modèle TAP semble séduisant, nous avons déjà remarqué dans la première partie qu’il ne permettait pas de couvrir toutes les situations de code fortement impératif que le langage C# autorise. Naturellement, c’est sur cette expérience que Microsoft a imaginé une nouvelle proposition que nous détaillerons dans le prochain article.
A bientôt
Bruno
Comments
Anonymous
June 23, 2013
Excellent résume de l'historique complet du parallélisme en .Net avec des explications "jusqu'au mode kernel". Bravo!Anonymous
June 23, 2013
Merci :-) Bruno