Comment garantir l'exécution de votre code grâce aux fonctionnalités de fiabilité de .NET Framework

Haute disponibilité

Stephen Toub
Cet article s'appuie sur une version préliminaire de .NET Framework 2.0. Toutes les informations contenues dans le présent document peuvent faire l'objet de modifications.

Consultez l'article en anglais 

*
Sur cette page
Description des exceptions OutOfMemoryException, StackOverflowException et ThreadAbortException Description des exceptions OutOfMemoryException, StackOverflowException et ThreadAbortException
Zones d'exécution limitéesZones d'exécution limitées
Finaliseurs critiques et descripteurs SafeHandleFinaliseurs critiques et descripteurs SafeHandle
FailFast et MemoryGatesFailFast et MemoryGates

Description des exceptions OutOfMemoryException, StackOverflowException et ThreadAbortException

Le code managé que vous écrivez est-il fiable ? Si votre responsable vous pose la question, vous répondrez « oui », naturellement. Vous utilisez des blocs try/finally pour libérer les ressources de façon déterministe et vous supprimez tous les objets possibles. Alors, bien sûr, votre code ne peut être que fiable, n'est-ce pas ? Vous faites du bon travail, non ?

Hélas, tout n'est pas si simple. Lors de l'écriture de code managé, la fiabilité exige la capacité à exécuter une séquence d'opérations d'une façon déterministe, même dans des conditions exceptionnelles. Ceci évite la perte des ressources et permet le maintien d'un état cohérent sans qu'il soit nécessaire de décharger le domaine d'application (ou pire, de redémarrer les processus) pour remédier au problème. Malheureusement, dans Microsoft® .NET Framework, les exceptions ne sont pas toutes déterministes et synchrones, d'où la difficulté d'écrire du code qui soit toujours déterministe dans sa capacité à exécuter une séquence prédéfinie d'opérations. Et avec .NET Framework 1.x, la chose est parfois pratiquement impossible. Dans cet article, je vais expliquer les raisons de cette difficulté, puis je présenterai les nouvelles fonctionnalités de .NET Framework 2.0 qui peuvent vous aider à la surmonter et à écrire du code plus fiable.

Pour bien montrer l'importance de la question, prenons SQL Server™ qui, depuis sa version 2005, peut héberger le CLR (Common Language Runtime), autorisant ainsi l'écriture de procédures stockées, de fonctions et de déclencheurs dans le code managé. L'accès aux procédures stockées devant être rapide, SQL Server héberge le CRL intra-processus. ASP.NET utilise le recyclage de processus pour garantir la haute disponibilité, en démarrant de nouveaux processus de travail lorsqu'il détecte un mauvais fonctionnement des processus de traitement en cours. Mais SQL Server, avec son hébergement intra-processus, n'a pas cette possibilité ; il ne peut tout simplement pas redémarrer le processus de travail, puisque le redémarrage de ce processus principal de la base de données entraîne un arrêt. Au lieu de cela, SQL Server choisit l'isolement du domaine d'application comme protection contre les échecs imprévus, de telle sorte qu'il soit possible de décharger le domaine endommagé et de le remplacer. En conséquence, il est capital que le code exécuté dans SQL Server soit aussi fiable que possible et que, en cas d'altération de l'état partagé, celle-ci soit limitée de façon à permettre la reprise du serveur. (Une altération au niveau processus peut forcer SQL Server à désactiver le CLR.) La fiabilité est importante pour les applications clientes qui utilisent des ressources système et particulièrement pour toutes les applications qui doivent fonctionner longtemps sans interruption, qu'il s'agisse de SQL Server, d'un service Windows® ou de tout autre programme ou hôte appelé à fonctionner longuement sans être interrompu. Il est donc essentiel de garantir la fiabilité de votre code pour en permettre l'exécution dans ce type d'environnement.


Les « méchants » de l'histoire !

Pour moi, une bonne pièce de théâtre est une pièce qui met en scène un protagoniste incompris, le rival que vous pourriez applaudir si la situation était autre. Dans notre histoire, les « méchants » ne se distinguent par rien de spécial, ils sont parfois utiles, et pourtant, pour la fiabilité du code, ils sont source de problème. Ils prennent la forme des exceptions OutOfMemoryException, StackOverflowException et ThreadAbortException.

Une exception OutOfMemoryException est générée quand le système tente d'obtenir pour un processus davantage de mémoire et que la mémoire contiguë ne suffit pas pour satisfaire la demande. Globalement, ces exceptions surviennent suite à des instructions explicites du code visant à créer de nouveaux objets, telles que des instructions newobj et newarr au niveau du langage MSIL (Microsoft intermediate language). Pourtant, d'autres opérations déclenchent aussi des allocations de mémoire. Par exemple, les conversions boxing (avec l'instruction MSIL box) exigent une allocation de tas pour stocker un type de valeur. L'appel à une méthode qui référence un type pour la première fois entraîne le chargement différé en mémoire de l'assembly approprié, ce qui exige des allocations. L'exécution d'une méthode encore non exécutée exige sa compilation en mode juste à temps (JIT), ce qui nécessite des allocations de mémoire pour stocker le code généré et les structures de données de runtime associées. Et ainsi de suite.

En réalité, les allocations de mémoire dans du code managé peuvent se produire aux endroits les plus improbables, ce qui complique le traitement correct des exceptions, même à l'aide de blocs try/catch/finally et de finaliseurs. Que se passe-t-il si ces finaliseurs et ces blocs du code d'annulation (back-out code) exigent des allocations de mémoire ? Que se passe-t-il si la compilation JIT n'a pas encore eu lieu ? Que se passe-t-il si la méthode appelée alloue de la mémoire, comme c'est le cas de nombreuses API de Framework ? En fait, le CLR de NET Framework 1.x ne garantit nullement l'exécution du code d'annulation. L'absence de garantie d'exécution du code d'annulation complique considérablement la création d'applications capables de traiter fiablement les conditions de type mémoire insuffisante.

L'exception StackOverflowException est également problématique. Cette exception survient lorsque la pile d'exécution pour le thread en cours déborde suite au trop grand nombre d'appels de méthode en attente, ce qui est souvent dû à une fonction fortement récursive ou à une trame de pile consommant beaucoup d'espace dans celle-ci, par exemple une trame qui utilise le mot clé C# stackalloc (équivalent de l'instruction localloc en MSIL). Certains appels de méthodes, comme les appels aux méthodes d'un autre domaine d'application (ce qui suppose l'intervention de .NET Remoting), peuvent également entraîner une surconsommation de la pile, même si la méthode cible en elle-même n'a pas besoin d'un espace considérable. Comme avec OutOfMemoryException, le CLR de .NET Framework 1.x ne garantit absolument pas l'exécution du code d'annulation ; en fait, il ne garantit même pas l'interception de l'exception StackOverflowException. Dans la version 1.x, une exception est générée si le dépassement de capacité a lieu dans le code managé, mais s'il a lieu dans le cadre du runtime, le processus sera détruit.

Lorsque Windows détecte un dépassement de capacité de la pile lié à l'exécution d'un thread, le processus peut tenter de gérer l'erreur. Cependant, si le code mis en œuvre par le processus pour traiter l'exception n'est pas écrit avec le plus grand soin, un nouveau dépassement risque de se produire. Si le même thread sature deux fois la pile sans réinitialiser sa page de garde, le système d'exploitation supprime le processus. La plupart des applications et des bibliothèques ne tentent jamais de traiter le problème car il est très difficile d'écrire du code capable de subir un dépassement de capacité de la pile après chaque appel de méthode (la gestion des dépassements exige aussi l'extraction d'une partie de la pile, ce qui signifie que les blocs finally proches du point de débordement risquent d'être ignorés). En fait, dans la version 1.x, le CLR lui-même pouvait entraîner un dépassement de capacité de la pile alors qu'il traitait un autre dépassement dû au code managé, ce qui obligeait le système d'exploitation à supprimer le processus. Dans NET Framework 2.0, le CLR peut détecter en toute fiabilité les dépassements de capacité de la pile, puis, sur la base d'une stratégie définie au niveau de l'hôte, supprimer le processus ou générer une exception et permettre l'exécution des blocs managés catch et finally adéquats.

Très clairement, des trois exceptions, la pire est ThreadAbortException. Lorsqu'un thread appelle Thread.Abort sur lui-même (par exemple, Thread.CurrentThread.Abort), le résultat est loin d'être appréciable : une exception ThreadAbortException synchrone est générée. Le problème de fiabilité intervient lorsqu'un thread utilise Abort pour mettre fin à un autre thread, ou lors de l'appel de AppDomain.Unload, ce qui a pour effet d'appeler Abort sur tous les threads en cours d'exécution dans le domaine cible ou qui ont une trame de pile provenant de ce domaine. Dans ce cas, le runtime injecte une exception ThreadAbortException dans le thread cible et celle-ci peut apparaître entre deux instructions machine quelconques. Examinons le code suivant :

IntPtr memPtr = Marshal.AllocHGlobal(0x100);
try
{
    ... // use allocated unmanaged memory here
}
finally { Marshal.FreeHGlobal(memPtr); }

Que se passe-t-il si une exception ThreadAbortException est générée après une allocation de mémoire mais avant l'accès au bloc try ? Le bloc finally n'est pas exécuté (puisque l'exception n'a pas été générée dans le bloc try correspondant) et vous vous retrouvez avec une perte de mémoire puisque le Garbage Collector (GC) ignore tout de cette mémoire non managée. Vous pouvez tenter de réécrire le code afin que l'allocation ait lieu dans le bloc try, mais cela ne servira à rien si l'exception survient après le renvoi de valeur par AllocHGlobal et avant que l'instruction machine ne s'exécute pour stocker cette valeur dans memPtr. Le pointeur vers la mémoire allouée sera perdu et vous perdrez de la mémoire.

Autre cas de figure : que se passe-t-il si l'exception est générée dans le bloc finally, ce qui peut se produire dans .NET Framework 1.x et, pour certains scénarios, dans .NET Framework 2.0 ? En bref, si le code utilisateur n'a aucun moyen de désigner les zones du code qui ne doivent pas être interrompues par des exceptions ThreadAbortExceptions, il est pratiquement impossible d'écrire du code fiable capable de résister aux exceptions asynchrones.

Haut de pageHaut de page

Zones d'exécution limitées

L'environnement .NET Framework 2.0 introduit la notion de zones d'exécution limitées (CER, Constrained Execution Regions) qui imposent des restrictions à la fois au runtime et au développeur. Dans une zone du code marquée CER, le runtime est soumis à des contraintes pour éviter de générer certaines exceptions asynchrones qui empêcheraient l'exécution de cette partie du code dans son intégralité. Le développeur doit également limiter les actions exécutables dans cette zone. Un mécanisme de contrôle est ainsi mis en œuvre pour faciliter l'écriture de code managé fiable, mécanisme qui va jouer un rôle clé dans les fonctionnalités de fiabilité proposées par .NET Framework 2.0.

Pour que le runtime puisse gérer cette contrainte, deux conditions sont mises en place pour les CER. Premièrement, le runtime retardera les abandons de thread pour le code qui s'exécute dans une zone CER. En d'autres termes, si un thread appelle la méthode Thread.Abort pour arrêter un autre thread en cours d'exécution dans une zone CER, le runtime n'arrêtera pas le thread cible tant que l'exécution ne sera pas terminée dans la CER. Deuxièmement, le runtime préparera les zones CER aussitôt que possible pour éviter les insuffisances de mémoire. Il fera donc en amont tout ce qu'il effectuait normalement durant la compilation JIT de la partie de code concernée. Il vérifiera aussi l'espace disponible dans la pile pour éviter des dépassements de capacité. Par ce travail préliminaire, le runtime peut plus facilement éviter des exceptions susceptibles de se produire dans la zone, qui empêcheraient le nettoyage correct des ressources.

De leur côté, pour utiliser efficacement les zones CER, les développeurs doivent éviter certaines actions susceptibles d'entraîner des exceptions asynchrones. Le code doit exclure certaines opérations, par exemple des allocations explicites, des conversions Boxing, des appels de méthodes virtuelles (à moins que la cible de l'appel de méthode virtuelle ne soit déjà préparée), des appels de méthode par réflexion, l'utilisation de Monitor.Enter (ou le mot-clé lock en C# et SyncLock dans Visual Basic®), des instructions isinst et castclass sur des objets COM, l'accès à des champs via des proxys transparents, la sérialisation et l'accès à des tableaux multidimensionnels.

En résumé, les CER constituent un moyen de déplacer les défaillances du code dues au runtime en faisant en sorte qu'elles interviennent avant son exécution (dans le cas d'une compilation JIT) ou après (en cas d'abandon de thread). Cependant, les CER contraignent réellement le code que vous écrivez. Des restrictions telles que l'interdiction de la plupart des allocations ou des appels de méthode virtuelle vers des cibles non préparées ne sont pas anodines, car elles impliquent un coût de développement élevé. En clair, il ne convient pas de définir des CER sur des ensembles de code à usage général volumineux ; il est préférable d'y voir une technique permettant de garantir l'exécution de petits blocs de code.


RuntimeHelpers

Par défaut, le runtime ignore si le code se trouve dans une zone d'exécution limitée. Un développeur doit explicitement marquer les zones du code à protéger et à préparer à l'aide de méthodes de la classe System.Runtime.CompilerServices.RuntimeHelpers. La méthode la plus importante de cette classe est la méthode statique PrepareConstrainedRegions. Cette méthode permet de vérifier si l'espace de la pile est suffisant et peut servir de marqueur pour le CLR, en l'informant qu'une zone CER est sur le point de commencer. Au niveau du langage MSIL, elle doit se placer immédiatement avant le début d'un bloc try, qu'elle rend « fiable » en garantissant que toutes les ressources nécessaires au code d'annulation sont allouées à l'avance (protégeant ainsi la zone contre des exceptions de type mémoire insuffisante provoquées par le CLR). Elle garantit également le report des abandons de thread jusqu'à ce que le code d'annulation soit terminé.

Notez que le seul code préparé est celui qui figure dans les blocs catch, finally, fault et filter associés au bloc try, et non celui du bloc try lui-même. Il est encore possible que le code du bloc try lève une exception OutOfMemoryException à partir de la compilation JIT ou soit interrompu par une exception ThreadAbortException. Cependant, puisque le code d'annulation a déjà été préparé, les blocs catch, finally, fault et filter associés pourront s'exécuter et gérer ces exceptions. (En l'absence de préparation, une condition de mémoire insuffisante pourrait empêcher l'exécution du code d'annulation.) Le code Visual Basic proposé dans la figure 1 montre le code qui sera préparé et celui qui ne le sera pas (bien qu'il ne fasse pas apparaître les blocs fault, puisque ceux-ci ne sont pas actuellement disponibles pour les langages Microsoft autres que le MSIL).

Étant donné que le code d'un bloc try marqué avec PrepareConstrainedRegions ne sera pas préparé, vous rencontrerez souvent (et vous le trouverez utile) le modèle suivant pour créer une zone de code à ne pas interrompre :

RuntimeHelpers.PrepareConstrainedRegions();
try {} finally
{
    ... // your noninterruptible code here
}

Ici, au lieu du bloc finally servant à annuler les changements d'état survenus dans le bloc try, vous avez un bloc finally contenant le code qui permet d'avancer.

Outre qu'elle prépare le code dans les blocs d'annulation, la méthode PrepareConstrainedRegions est transitive, ce qui veut dire que le CLR va parcourir le graphe d'appel à partir du code d'annulation et préparer les méthodes qui s'y trouvent. Cependant, il existe certaines restrictions concernant les méthodes qui seront préparées. Premièrement, le CLR ne peut préparer que les méthodes qu'il trouve. Si un site d'appel implique une indirection quelconque, comme un appel via une interface, un délégué, une méthode virtuelle ou une réflexion, le CLR ne pourra pas parcourir le graphe jusqu'à la cible et celle-ci ne sera donc pas préparée. En conséquence, des exceptions d'insuffisance de mémoire risquent toujours d'être générées au moment de l'exécution et les abandons de thread ne seront pas correctement différés. Deuxièmement, chaque méthode du graphe doit être couverte par un contrat de fiabilité approprié.


Contrats de fiabilité

Pour que le runtime prépare efficacement une zone CER, il doit savoir que le code qu'elle contient et son graphe d'appel respectent les contraintes nécessaires pour une exécution dans la CER. Pour cette raison, .NET Framework fournit l'attribut ReliabilityContractAttribute (montré dans la figure 2) dans l'espace de noms System.Runtime.ConstrainedExecution.

Un contrat de fiabilité exprime deux concepts qui, bien que différents, sont reliés : quel type d'altération d'état des exceptions asynchrones générées pendant l'exécution de la méthode peuvent-elles entraîner, et quel type de garanties d'exécution la méthode peut-elle fournir si elle doit s'exécuter dans une CER ? Il est possible de spécifier des contrats non seulement au niveau de la méthode, mais aussi au niveau de la classe et de l'assembly. Les contrats appliqués aux méthodes remplacent ceux du niveau classe ou assembly et ceux du niveau classe remplacent ceux du niveau assembly.

La première partie du contrat est exprimée par le biais de la propriété ConsistencyGuarantee de l'attribut, qui prend une valeur dans l'énumération Consistency. Cette propriété décrit le niveau d'altération de l'état qui peut résulter d'une exception asynchrone générée lors de l'exécution de la méthode. (Imaginez qu'il s'agit d'un indicateur qui fournit à l'appelant des renseignements sur les éléments d'état à éliminer afin de retrouver un état correct.) La pire des altérations est l'altération de processus (MayCorruptProcess), qui indique que la méthode en question a éventuellement interféré avec un état utilisé à l'échelle du processus au moment où l'exception a été générée, au point que cet état risque désormais d'être incohérent. De même, MayCorruptAppDomain signale que la méthode a pu interférer avec un état isolé du domaine d'application (par exemple, une variable statique) et, par conséquent, cet état serait incohérent (bien que l'état au niveau processus soit resté cohérent). MayCorruptInstance sert à signaler que la méthode a peut-être laissé une instance dans un état incohérent (mais rien de plus) et WillNotCorruptState signifie que la méthode n'a pu en rien générer un état incohérent (ce qui est souvent le cas pour des méthodes qui lisent simplement des informations d'état).

La propriété Cer et l'énumération sont utilisées pour indiquer quelle sorte de garanties d'exécution offre une méthode si elle est exécutée dans une zone CER (hors d'une CER, cette garantie n'a aucune valeur). La valeur Success signifie que la méthode sera toujours exécutée correctement dans une CER, en supposant que les valeurs fournies en entrée soient valides ; en effet, une méthode marquée Success peut encore générer des exceptions si elle est associée à des paramètres qui ne sont pas valides.

La valeur Cer pour MayFail permet de signaler que, face à des exceptions asynchrones, le code risque de ne pas s'exécuter comme prévu. Étant donné que les abandons de thread sont reportés dans les zones d'exécution limitées, cela signifie que votre code effectue une opération susceptible d'entraîner une allocation de mémoire ou d'aboutir à un dépassement de capacité de la pile. Plus grave, cela veut dire que vous devez prendre en considération les défaillances possibles en cas d'appel de cette méthode.

Seules trois combinaisons de valeurs Cer et Consistency sont valides pour les méthodes figurant dans le graphe d'appel d'une racine CER :

[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]

La première indique que, dans des conditions exceptionnelles, la méthode peut échouer mais au pire elle n'endommagera que l'instance spécifique ; le domaine d'application et l'état au niveau processus seront intacts. La seconde indique que la méthode peut échouer, mais même si c'est le cas, tous les états seront valides. La communauté C++ parle dans ce cas de « garantie d'exception forte », à savoir que soit l'action a été exécutée, soit elle a échoué mais sans effet pervers ni génération d'exception. La troisième implique que la méthode réussit toujours et n'endommage aucun état. Cette dernière combinaison est la garantie la plus solide possible, mais par suite, très peu de méthodes peuvent être marquées comme telles. En fait, la plupart des méthodes auxquelles vous aurez affaire seront marquées avec la valeur Cer ou None, ou n'auront pas de contrat de fiabilité du tout et, dans ce cas, elles n'offriront aucune garantie si une exception survient. L'absence de contrat de fiabilité sur une méthode implique Cer.None et Consistency.MayCorruptProcess.

Lorsque vous définissez des contrats de fiabilité sur vos propres méthodes, vous devez considérer qu'il s'agit d'une « rupture » qui réduit les garanties Cer ou Consistency. En effet, les appelants peuvent avoir défini une dépendance par rapport à votre niveau de fiabilité et en changeant les niveaux, vous rompez ces dépendances. Le fait d'exclure les contrats de fiabilité de vos méthodes est une première approche raisonnable, du moins tant que vous ne savez pas exactement quelles méthodes risquent d'être utilisées dans les zones CER.

Les contrats de fiabilité posent également certains problèmes d'interprétation intéressants. Par exemple, un des problèmes rencontrés par les concepteurs CLR est que la plupart des méthodes bien implémentées vérifient leurs paramètres et génèrent des exceptions pour les entrées non valides. Ainsi, une approche consistant à simplement interdire toutes les allocations dans des zones CER se serait avérée inutile (certaines méthodes peuvent aussi procéder à des allocations puis rétablir la situation après des exceptions de type mémoire insuffisante). Les contrats de fiabilité sont une tentative pour éliminer ce type d'éléments du runtime, permettant à l'auteur du code de stipuler clairement si les appelants doivent se soucier des échecs et, si oui, quelle proportion de l'état concerné doit être éliminée. En conséquence, les contrats de fiabilité sont considérés comme nuls et non avenus si les arguments passés à la méthode sont illégaux ou si la méthode est incorrectement utilisée. De plus, certains concepts s'expriment difficilement dans les contrats. Imaginez une méthode qui accepte un objet comme paramètre et appelle sa méthode Equals. Cette méthode peut être fiable, mais uniquement si vous passez un objet qui a une substitution Equals fiable. Il n'existe actuellement aucun dispositif intégré pour exprimer ce concept.


Préparation explicite

Comme indiqué plus haut, PrepareConstrainedRegions impose au runtime de parcourir les graphes d'appel à partir de la racine CER. Malheureusement, le runtime n'est pas omniscient et il ne peut pas prévoir quelle sera la méthode cible réelle à partir des sites d'appel virtuel. Par conséquent, si une interface, une méthode virtuelle, un délégué ou une méthode générique est utilisé depuis une zone restreinte, le runtime risque de pas pouvoir préparer la méthode cible suffisamment tôt puisque celle-ci ne sera pas déterminée avant l'exécution (ou, dans le cas de génériques, elle peut l'être mais une allocation de mémoire risque encore d'être nécessaire la première fois que la méthode sera appelée avec des paramètres à type unique). Pour aider le CLR, un développeur a la possibilité d'utiliser deux méthodes supplémentaires de la classe RuntimeHelpers, PrepareMethod et PrepareDelegate. PrepareMethod accepte un handle RuntimeMethodHandle pour la classe MethodBase de la méthode cible. Au moment de l'exécution, le code peut extraire le gestionnaire de la méthode réelle à appeler, puis utiliser PrepareMethod pour la préparer avant l'accès à la zone CER.

Par exemple, examinons le premier extrait de code de la figure 3. BaseObject expose une méthode virtuelle VirtualMethod qui est utilisée dans une CER. Du fait qu'elle est virtuelle, le CLR ne peut pas déterminer par l'analyse statique quelle sera la cible réelle de l'appel. Si l'objet passé à SomeMethod est un DerivedObject, la substitution VirtualMethod de DerivedObject peut ne pas être correctement préparée avant l'appel, ce qui met la zone CER en péril. Pour résoudre la situation, il est possible d'utiliser PrepareMethod une fois que la cible réelle est connue. Naturellement, le recours à la réflexion pour obtenir des descripteurs (handles) de méthode peut coûter cher. Si vous savez quelles méthodes seront appelées, vous pouvez envisager de procéder d'emblée à une préparation, par exemple dans un constructeur statique.

Dans le même style que PrepareMethod, vous pouvez utiliser PrepareDelegate qui accepte un délégué et prépare la méthode référencée. PrepareDelegate prépare uniquement le délégué spécifique référencé, elle ne prépare pas des délégués liés par celui-ci. En d'autres termes, même si un délégué multicast est composé de plusieurs délégués, seul celui qui est référencé est préparé. Si un délégué doit apparaître dans une zone CER, la liste d'appel correspondante doit être extraite et chaque délégué doit faire l'objet d'une préparation, ou chaque délégué doit être préparé avant d'être combiné aux autres. Ceci a des conséquences pour les événements. Si vous comptez déclencher un événement depuis une zone CER, vous devez lui fournir un accesseur add personnalisé qui préparera les délégués inscrits, avant d'associer le délégué fourni à ceux précédemment inscrits :

public event EventHandler MyEvent {
    add {
        if (value == null) return;
        RuntimeHelpers.PrepareDelegate(value);
        lock(this) _myEvent += value;
    }
    remove { lock(this) _myEvent -= value; }
}

C'est ainsi que plusieurs événements de AppDomain sont mis en œuvre, tels que ProcessExit et DomainUnload, les deux étant déclenchés depuis une CER implicitement enracinée dans le CLR.

Il peut être difficile de détecter des problèmes lorsque PrepareMethod et PrepareDelegate ne sont pas utilisées là où elles devraient l'être. Le CLR fournit plusieurs assistants de débogage managés (MDA, managed debug assistants) pour diagnostiquer les problèmes liés aux CER. Pour plus d'informations, reportez-vous à l'encadré sur Managed Debug Assistants (les assistants de débogage managés).


Gestion de StackOverflowException

Le CLR ne garantit pas qu'une exception StackOverflowException dans un bloc try permettra l'exécution du code d'annulation adéquat. Pour tenir compte des situations où le code d'annulation doit absolument s'exécuter en réponse à des exceptions de type StackOverflowException, la classe RuntimeHelpers fournit la méthode ExecuteCodeWithGuaranteedCleanup. Cette méthode accepte trois paramètres : un délégué RuntimeHelpers.TryCode contenant le code à exécuter dans le bloc try, un délégué RuntimeHelpers.CleanupCode contenant le code à exécuter dans le bloc finally et des données utilisateurs qui seront fournies aux deux délégués. Un exemple d'utilisation de cette méthode est fourni par le code de la figure 4.

ExecuteCodeWithGuaranteedCleanup génère un gestionnaire des exceptions structuré (SEH, structured exception handling) dans le CLR. Avec cette protection, la méthode rappelle le code TryCode managé. S'il se produit un dépassement de capacité de pile dans ce code, le runtime l'intercepte et s'assure qu'il reste suffisamment d'espace dans la pile pour exécuter correctement le CleanupCode. Naturellement, vous devez veiller à ce que le code d'annulation lui-même ne sature pas la pile (il ne doit pas effectuer d'appels fortement récursifs ni dépendre de code nécessitant un espace important, voire indéterminé, dans la pile). Accessoirement, notez que C# ne permet pas pour l'instant de spécifier un attribut personnalisé sur une méthode anonyme ; évitez donc d'utiliser des méthodes anonymes pour votre code d'annulation.


Hôtes CLR et stratégies de traitement spécial

Dans quel cas une condition de type mémoire insuffisante n'entraîne-t-elle pas une exception OutOfMemoryException ? Lorsqu'un hôte CLR stipule qu'elle ne le doit pas. Une application non managée hébergeant le CLR peut contrôler la manière dont le runtime réagit à certains types de situations, notamment les problèmes d'allocation de ressources, les échecs d'allocation de ressources dans des zones critiques du code, des verrous orphelins et des erreurs fatales dans le runtime lui-même. Via l'interface non managée ICLRPolicyManager, un hôte peut contrôler les actions prises par le CLR en présence de certaines erreurs. Ces actions incluent la génération d'exceptions, l'abandon de threads, le déchargement du domaine d'application, la sortie du processus, la désactivation du runtime et l'absence de toute action (défaillance ignorée). La génération d'exceptions et le fait d'ignorer les échecs sont les mesures appliquées par l'hôte CLR par défaut. Ainsi, par exemple, lorsqu'il est impossible d'allouer de la mémoire, le runtime lève une exception OutOfMemoryException ; lorsqu'il est averti de l'existence d'un verrou orphelin, il ignore le problème. Avec ICLRPolicyManager et sa méthode SetActionOnFailure, un hôte peut changer ces comportements par défaut. Ainsi, en cas d'impossibilité d'allouer de la mémoire, l'hôte peut demander au runtime d'abandonner le thread en générant une exception ThreadAbortException au lieu d'une OutOfMemoryException. Lorsqu'un verrou orphelin est détecté, l'hôte peut faire en sorte que le runtime décharge le domaine d'application au lieu d'ignorer l'erreur. Quand vous écrivez votre code, n'oubliez pas que cette démarche est possible.

Ces types de stratégies ne suffisent pas pour tous les hôtes. Parfois, l'hôte doit pouvoir prendre une mesure à un niveau supérieur si la précédente ne suffit pas. Par exemple, examinons ce qui se produit lorsqu'une exception ThreadAbortException est générée dans un bloc try et que le bloc finally associé entre dans une boucle sans fin. Avec la stratégie par défaut du runtime, le thread concerné ne s'arrêtera jamais et un hôte non managé qui a besoin de garanties de fiabilité doit pouvoir, d'une manière ou d'une autre, régler ce problème. La méthode SetTimeoutAndAction de ICLRPolicyManager (et, à un moindre degré, sa méthode SetDefaultAction) apporte une solution. Cette méthode accepte trois paramètres : une action, une temporisation et une réponse. Si le runtime est en train d'exécuter une action conditionnée à un délai de temporisation et si celui-ci est écoulé, le runtime déclenche une mesure de réponse. Par exemple, un hôte va spécifier que tous les abandons de thread ne doivent pas durer plus de 5 secondes et si l'un d'eux dépasse ce délai, le domaine d'application sera déchargé. Le runtime lui-même ne dispose que d'un délai de temporisation par défaut, applicable à la fermeture du processus. Si une tentative visant à quitter proprement un processus (abandon de tous les threads, déchargement de tous les domaines d'application, etc.) n'aboutit pas au bout de 40 secondes, le runtime met un terme au processus.

J'ai signalé plus haut qu'un hôte peut prendre plusieurs mesures en réponse à certains échecs, y compris l'abandon de threads et le déchargement des domaines d'application. Ce que je n'ai pas dit, c'est que certaines de ces mesures sont associées à des niveaux différents de gravité, notamment les abandons de thread, les déchargements de domaine d'applications et la sortie de processus. Jusqu'à présent, j'ai simplement évoqué les abandons de threads suite à la génération par le runtime d'une ThreadAbortException sur un thread. En général, cette exception met un terme au thread. Cependant, un thread peut gérer un abandon, ce qui évite qu'il ne se termine. Pour ce faire, le runtime propose une action plus performante, nommée abandon de thread brutal (rude thread abort). Ce type d'abandon met un terme à l'exécution du thread. Dans ce cas, le CLR ne garantit pas que le code d'annulation sur le thread va s'exécuter (à moins qu'il ne s'exécute dans une zone CER). « Brutal » est bien le mot qui convient.

De même, alors qu'un déchargement du domaine d'applications classique arrête normalement tous les threads du domaine, un déchargement brutal va mettre brutalement fin à tous les threads du domaine en question et le runtime ne garantit nullement que les finaliseurs normaux associés aux objets de ce domaine seront exécutés. SQL Server 2005 est un hôte CLR qui utilise des abandons de threads brutaux et des déchargements de domaines d'applications brutaux lorsqu'il adopte une mesure de niveau supérieur. Lorsqu'une exception asynchrone se produit, l'échec d'allocation de ressources donne lieu à un abandon de thread. Et lorsque l'abandon de thread se produit, s'il ne finit pas dans le temps imparti par SQL Server, l'action supérieure est déclenchée, à savoir l'abandon brutal. De la même manière, si le déchargement du domaine d'applications ne prend pas fin dans le délai prévu par SQL Server, l'action supérieure, à savoir le déchargement brutal, est mise en œuvre. (Notez que les stratégies évoquées ci-dessus ne sont pas exactement celles qu'utilise SQL Server, puisque ce dernier vérifie également si le code s'exécute dans des zones critiques, mais nous allons revenir sur ce point).

Il est important de comprendre la différence de traitement par le CLR des abandons de thread normaux et des abandons brutaux. Dans .NET Framework 1.x, il n'existe pas d'abandon de thread brutal et les abandons de thread peuvent être générés n'importe où dans le code managé, sans que le CLR n'apporte aucune protection. Dans .NET Framework 2.0, le CLR retarde les abandons de thread normaux par défaut en les renvoyant dans les zones CER, les blocs finally et catch, les constructeurs statiques et le code non managé. Cependant, les abandons de thread brutaux ne seront reportés que vers les zones CER et le code managé (le CLR n'a que peu de contrôle, voire pas du tout, sur ce dernier).

Les abandons de threads brutaux et les déchargements brutaux du domaine d'applications sont employés par les hôtes CLR pour garantir la maîtrise du code défaillant. Naturellement, l'impossibilité d'exécuter des finaliseurs ou des blocs finally extérieurs aux zones CER en raison de ces actions présente pour l'hôte CLR de nouveaux problèmes de fiabilité, puisque ces actions vont très probablement provoquer une perte des ressources que le code d'annulation était censé nettoyer. Heureusement, il y a les finaliseurs critiques.

Haut de pageHaut de page

Finaliseurs critiques et descripteurs SafeHandle

Un abandon de thread normal permet l'exécution des blocs finally appropriés, ce qui n'est pas garanti avec les abandons de thread brutaux (sauf dans les zones CER). Un déchargement de domaine d'applications en bonne et due forme utilise des abandons de threads normaux et permet l'exécution des blocs finally et des finaliseurs pour les objets du domaine concerné, mais là encore, la garantie ne vaut pas pour les déchargements brutaux (sauf dans une CER). En cas de déchargement brutal du domaine d'applications, le runtime ne garantit pas que le code d'annulation ou les finaliseurs normaux seront exécutés. Comment, dans ces conditions, un système peut-il être fiable ?

L'environnement .NET Framework 2.0 introduit un nouveau type de finaliseur, dit finaliseur critique. Les finaliseurs critiques s'exécutent même pendant un déchargement brutal du domaine d'applications et ils s'exécutent dans une CER. Leur utilisation doit être réservée à des cas importants, lorsque la sécurité et la fiabilité sont prioritaires. Seul un petit nombre de classes du .NET Framework y a recours.

Pour mettre en œuvre un finaliseur critique, il suffit de dériver la classe en question de la classe CriticalFinalizerObject dans l'espace de noms System.Runtime.ConstrainedExecution.

[SecurityPermission(
    SecurityAction.InheritanceDemand, UnmanagedCode=true)]
public abstract class CriticalFinalizerObject
{
    protected CriticalFinalizerObject();
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    protected override void Finalize();
}

Les finaliseurs que vous implémentez dans votre classe dérivée seront appelés dans une zone CER et ils doivent offrir des garanties de fiabilité correspondant à celles du contrat de fiabilité de CriticalFinalizerObject.Finalize. En d'autres termes, vous ne devez dériver une classe de CriticalFinalizerObject que si le finaliseur est assuré de réussir et s'il offre la garantie de ne corrompre aucun état en cas d'appel dans une CER. Lorsqu'un objet dérivé de CriticalFinalizerObject est instancié, le runtime prépare immédiatement la méthode Finalize, afin de ne pas avoir à compiler le code JIT (ni à procéder à aucune autre préparation) lorsque viendra le moment d'exécuter la méthode pour la première fois.

Une des classes de l'environnement .NET Framework qui dérive de CriticalFinalizerObject est SafeHandle, qui se trouve dans l'espace de noms System.Runtime.InteropServices. La classe SafeHandle est un ajout bienvenu dans .NET Framework et elle joue un rôle important pour résoudre de nombreux problèmes de fiabilité existants dans les versions précédentes. À la base, SafeHandle est un simple wrapper managé autour d'un pointeur IntPtr avec un finaliseur qui sait comment libérer la ressource sous-jacente référencée dans ce IntPtr. Du fait que SafeHandle est dérivée de CriticalFinalizerObject, ce finaliseur est préparé lorsque SafeHandle est instanciée et il sera appelé depuis une zone CER pour garantir que les abandons de threads asynchrones n'interrompent pas le finaliseur.

Prenons la fonction Win32 FindFirstFile, utilisée pour énumérer les fichiers d'un répertoire :

HANDLE FindFirstFile(
    LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData);

Dans .NET Framework 1.x, une déclaration P/Invoke pour cette fonction se présente 
généralement comme suit : 
[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
private static extern IntPtr FindFirstFile(
    string pFileName, [In, Out] WIN32_FIND_DATA pFindFileData);

Malheureusement, si une exception asynchrone est générée après le renvoi d'une valeur par 
FindFirstFile mais avant le stockage consécutif de IntPtr, la ressource du système d'exploitation 
est perdue sans qu'il y ait espoir de la libérer. C'est alors que SafeHandle intervient. 
Dans .NET Framework 2.0, vous pouvez réécrire la signature comme suit : 
[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
private static extern SafeFindHandle FindFirstFile(
    string fileName, [In, Out] WIN32_FIND_DATA data);

La seule différence est que j'ai remplacé le type de renvoi IntPtr par SafeFindHandle, où SafeFindHandle est un type personnalisé dérivé de SafeHandle. Lorsque le runtime appelle FindFirstFile, il crée tout d'abord une instance de SafeFindHandle. Lorsque FindFirstFile renvoie une valeur, le runtime stocke le pointeur IntPtr résultant dans l'instance déjà créée de SafeFindHandle. Le runtime garantit que cette opération est atomique, c'est-à-dire que si le retour de la méthode P/Invoke est correct, IntPtr sera stocké en toute sécurité dans SafeHandle. Une fois dans SafeHandle, même si une exception asynchrone se produit et empêche le stockage de la valeur SafeFindHandle renvoyée par FindFirstFile, le pointeur IntPtr adéquat est déjà stocké dans un objet managé dont le finaliseur assurera la libération en bonne et due forme.

En interne, .NET Framework utilise un grand nombre de types dérivés de SafeHandle, un pour chaque type de ressource non managée qu'il doit traiter. Publiquement, seuls quelques-uns sont exposés, notamment SafeFileHandle (utilisé pour contenir les descripteurs de fichiers) et SafeWaitHandle (pour contenir les descripteurs de synchronisation). Naturellement, si vous avez affaire à des ressources non managées qui ne sont pas couvertes par ces types, vous pouvez créer vos propres types dérivés SafeHandle. La figure 5 montre quelques exemples de création. Pour faciliter l'écriture, .NET Framework fournit deux types publics dérivés SafeHandle, SafeHandleZeroOrMinusOneIsInvalid et SafeHandleMinusOneIsInvalid. Le type SafeHandle doit pouvoir indiquer si le pointeur IntPtr qu'il stocke est valide pour le type de ressource concerné. Étant donné que la majorité des descripteurs de ressources dans l'univers Win32 ne sont pas valides lorsqu'ils sont égaux à -1, ou lorsqu'ils sont égaux à 0 ou à -1, ces classes sont prévues pour incorporer ces vérifications afin que vous n'ayez pas à les mettre dans votre code.

Outre la gestion de la durée de vie des ressources, les descripteurs SafeHandle offrent d'autres avantages. Premièrement, ils facilitent la gestion de la mémoire en réduisant les promotions de graphe pour la finalisation. Dans .NET Framework 1.x, une classe qui exige des ressources non managées stocke habituellement le pointeur IntPtr adéquat, en même temps que d'autres objets managés dont elle a besoin. Cette classe implémente presque toujours un finaliseur pour garantir la libération correcte de IntPtr. Comme les objets finalisables survivent toujours à au moins un nettoyage de la mémoire, le graphe d'objet complet, à commencer par la classe en question, survit lui-aussi à ce nettoyage, à cause précisément du pointeur IntPtr. Étant donné que SafeHandle remplace désormais le IntPtr et qu'il a son propre finaliseur, le plus souvent la classe qui stocke SafeHandle n'a plus besoin de son propre finaliseur. Ceci supprime la promotion de graphe GC, réduisant la charge imposée à l'opération de nettoyage. De plus, la suppression du finaliseur signifie que vous pouvez laisser tomber les appels à GC.KeepAlive(this) dans ces objets.

Les descripteurs SafeHandle aident également à combler les failles éventuelles de sécurité dues aux problèmes de recyclage des descripteurs. Windows gère des tables internes qui mettent en correspondance des descripteurs avec les objets du noyau associés. Lorsqu'un descripteur est libéré, Windows peut le réutiliser pour pointer vers une autre ressource. Dans certains cas, un pirate peut utiliser plusieurs threads afin d'exploiter le recyclage des descripteurs, en fermant le descripteur sur un thread et en l'utilisant sur un autre, et en espérant que Windows réaffectera le descripteur à une ressource différente, ce qui lui permet d'accéder à une ressource qui, sinon, ne serait pas disponible. Pour réduire cette possibilité à néant, SafeHandle implémente un comptage de références. Lorsqu'un SafeHandle est passé à une méthode P/Invoke, le runtime incrémente son compteur d'utilisation. Lorsque P/Invoke répond, le compteur est décrémenté. Lorsqu'un appel à Dispose ou Close est effectué sur un descripteur SafeHandle, son compteur d'utilisation est vérifié. Si le compteur est à 0, l'opération peut se poursuivre. S'il est supérieur à 0, l'opération est différée jusqu'à ce que le compteur passe à 0. De cette manière, il n'est pas possible de lancer une attaque par recyclage de descripteur via SafeHandle. Ce comptage de références génère un coût minime. Mais si, pour une raison quelconque, ce coût reste trop élevé pour vous, vous pouvez utiliser à la place la classe CriticalHandle, qui appartient également à l'espace de noms System.Runtime.InteropServices. La classe CriticalHandle est similaire à SafeHandle, si ce n'est qu'elle ne met pas en œuvre un comptage des références.

L'utilisation de SafeHandle est simple. Examinez l'implémentation de SafeLibraryHandle montrée à la figure 5. Cette classe sert à stocker un descripteur dans une bibliothèque fournie par LoadLibrary. Comme LoadLibrary utilise un comptage des références pour vérifier que les bibliothèques ne sont libérées qu'après libération de toutes les références, il est important de s'assurer qu'à chaque appel à LoadLibrary est associé un appel correspondant à FreeLibrary. Et comme LoadLibrary renvoie un descripteur, la procédure convient parfaitement à SafeHandle. Une requête très courante dans .NET Framework 1.x était de permettre l'utilisation de la fonctionnalité P/Invoke pour appeler une fonction non managée liée dynamiquement au moment de l'exécution. La figure 6 montre un exemple de ce type de requête avec .NET Framework 2.0.


Zones critiques

Les zones critiques servent à indiquer que le code qui s'y exécute maintient un verrou et qu'il est peut-être en train de modifier un état partagé. Essentiellement, elles sont une autre forme de comptage de verrous.

L'impact d'un abandon de thread ou d'une exception non gérée dans une zone critique peut ne pas se limiter à la tâche en cours (voir l'encadré Unhandled Exceptions (Exceptions non gérées) pour avoir une idée des modifications concernant les exceptions non gérées dans .NET Framework 2.0). Examinons l'extrait de code suivant, qui utilise un membre statique variable _someSharedLock :

lock(_someSharedLock) { /* do important stuff here */ }

Examinons à présent ce qui se passe en cas d'abandon de thread brutal dans le verrou. Le thread abandonné pouvait très bien être en train de modifier un état partagé et, dans ce cas, il y a de fortes chances pour que cet état soit désormais incohérent. En outre, ce verrou est maintenant orphelin en raison de l'arrêt du thread, lequel n'a jamais pu quitter le verrou en raison de son arrêt prématuré. D'autres threads pourraient très facilement se bloquer aussi, puisque le verrou on _someSharedLock ne sera jamais libéré. Le mieux, du point de vue de la fiabilité, est de supprimer tout le domaine d'applications dans lequel s'exécutait le thread.

Pour que les hôtes CLR disposent de cette option, le code managé peut déclarer à quel moment il va entrer dans une zone critique et la quitter, et dans cette zone, l'hôte peut dès lors supprimer le domaine d'applications complet en cas de problème. Ceci est possible grâce aux nouvelles méthodes statiques BeginCriticalRegion et EndCriticalRegion de la classe Thread. Les hôtes sont avertis lorsqu'un thread est abandonné à partir d'une zone critique et ils peuvent choisir de gérer la situation au mieux. (Les hôtes qui procèdent à des allocations de mémoire managée sont également informés des demandes de mémoire émises dans les zones critiques, ce qui leur permet d'y répondre en priorité et de réduire ainsi les risques d'échec dans ces zones.) SQL Server tire parti de cette possibilité dans sa stratégie de traitement spécial des échecs, comme le montre l'exemple de la figure 7. S'il y a abandon d'un thread dans une zone critique, un verrou est posé, le code risque d'être en train de modifier l'état partagé et, par conséquent, une action de niveau supérieur est déclenchée, telle qu'un déchargement du domaine d'applications.


Figure 7 Exemple de stratégie de traitement spécial par l'hôte

Dans mon précédent exemple, pour expliquer les zones critiques, j'ai utilisé le mot-clé C# lock. Cependant, ce n'est pas vraiment un bon exemple. Le mot-clé C# lock (SyncLock dans Visual Basic) est construit autour de la classe Monitor, qui contient déjà son système de notification propre. D'autres mécanismes de verrouillage ne sont pas aussi simples que Monitor.

Examinons des événements de réinitialisation automatique. Que signifie pour un thread le fait d'« acquérir » un événement de réinitialisation automatique ? Pas grand-chose. En fait, le runtime a besoin de l'aide d'un développeur pour savoir à quel moment le code s'exécute dans une zone critique. Par conséquent, vous devez envisager d'utiliser Thread.BeginCriticalRegion et Thread.EndCriticalRegion chaque fois que votre code utilise un EventWaitHandle (d'où sont dérivés ManualResetEvent et AutoResetEvent dans .NET Framework 2.0), un sémaphore, un verrou spinlock (voir l'article de Jeffrey Richter dans ce numéro de MSDN®Magazine pour plus d'informations sur les spinlocks), un mécanisme de verrouillage managé personnalisé ou tout autre mécanisme de verrouillage non managé auquel vous accédez via P/Invoke.

Haut de pageHaut de page

FailFast et MemoryGates

Les CER et les zones critiques permettent au runtime et à un hôte CLR de rendre le code plus fiable. Parfois cependant, il se produit un événement tel que vous voulez prendre les choses en main et supprimer votre application aussi vite que possible pour éviter une situation pire encore. Dans ce cas, vous avez besoin d'une méthode permettant de mettre fin rapidement au processus de l'application. Utilisez FailFast.

System.Environment.FailFast est une méthode simple qui exécute trois opérations : Premièrement, elle écrit un événement dans le journal des applications Windows, indiquant qu'une erreur fatale s'est produite. Ce message contient des informations personnalisées transmises à FailFast sous la forme d'un paramètre de chaîne unique. Deuxièmement, FailFast déclenche un rapport d'erreur Watson et un mini-vidage, qui sont enregistrés dans le service Microsoft Windows Error Reporting (WER). Vous pouvez ensuite utiliser les services Windows Quality Online Services (winqual.microsoft.com) pour accéder aux données WER sur l'application et les analyser afin de localiser le problème. Troisièmement, FailFast supprime le processus.

FailFast gère plutôt bien les situations consécutives à un problème. Mais qu'en est-il des vérifications en amont pour vérifier que rien ne va poser problème ? Une « memory gate », sorte de barrière de protection de la mémoire, permet de vérifier si les ressources sont suffisantes, avant de lancer une activité exigeant beaucoup de mémoire. Si la vérification donne un résultat négatif, l'opération ne démarre pas, ce qui réduit le risque d'échec d'exécution d'une application suite à une insuffisance de ressources.

Les memory gates sont mises en œuvre dans .NET Framework 2.0 via la classe System.Runtime.MemoryFailPoint. Pour utiliser une memory gate, créez une instance de MemoryFailPoint, en transmettant à son constructeur le nombre de méga-octets de mémoire que l'opération à venir doit utiliser. Si la quantité de mémoire spécifiée n'est pas disponible, une exception InsufficientMemoryException (dérivée de OutOfMemoryException) est générée. Ceci peut être utile car l'exception est générée avant le début des opérations et non pendant leur déroulement, ce qui évite dans certains cas des dépendances avec le code d'annulation. En général, l'utilisation de MemoryFailPoint est la suivante :

using(new MemoryFailPoint(10)) //operation will require 10 MB of memory
{
    ... // perform operation
}

Dans l'implémentation actuelle de MemoryFailPoint, le système vérifie si la quantité de mémoire spécifiée est disponible, mais il ne la réserve pas. Il est possible qu'un autre thread ou un autre processus survienne et réclame ces ressources. Pourtant, les memory gates ont jusqu'à présent été employées avec succès et elles restent toujours utiles pour assurer une vérification déterministe de la consommation des ressources.


Conclusion

L'écriture de code fiable capable de faire face à toutes les situations défavorables peut s'avérer une tâche décourageante. Toutefois, à moins d'écrire un framework ou une bibliothèque pour les hôtes CLR qui doivent fonctionner longtemps sans interruption, vous n'aurez sans doute pas à vous faire trop de souci à ce sujet. Mais pour ceux qui doivent en passer par là, .NET Framework 2.0 offre un ensemble d'outils performants qui facilitent le travail. Si vous comprenez clairement le mode de fonctionnement et d'utilisation de ces systèmes, vous pouvez écrire du code managé aussi fiable qu'un code non managé rédigé avec soin.


Haut de pageHaut de page