Comportement en temps réel de .NET Compact Framework
Maartin Struys
Michel Verhagen
PTS Software
S'applique à :
Microsoft® Windows® CE .NET
Microsoft Visual
Studio® .NET
Microsoft Visual Basic® .NET
Microsoft Visual C#®
Microsoft .NET Compact
Framework
Téléchargez CFInRT_used_for_actual_measurements.exe.
Téléchargez RTCF.exe.
Téléchargez Win32CEAppInRT.exe.
Résumé : Avec la sortie de Visual Studio .NET 2003, qui intègre la prise en charge de Smart Device Programmability, il est possible de développer des applications pour une vaste gamme de périphériques à partir de code géré. Les développeurs de logiciels peuvent désormais utiliser des langages captivants comme Visual Basic .NET ou Visual C# pour le développement de périphériques. Si cela semble prometteur, une question demeure : est-il possible d'utiliser les fonctionnalités temps réel de Windows CE .NET et d'écrire en code géré des applications destinées à un appareil embarqué ? Cet article répond à cette question et propose un scénario permettant d'associer le comportement en temps réel et les fonctionnalités propres à Microsoft .NET.
Sommaire
Environnement géré et environnement
non géré
La
fonction Platform Invoke
Un
scénario en temps réel
Le test en
lui-même
Les
résultats
Les
pièges
La
vérification des résultats
Conclusion
Remerciements
Environnement géré et environnement non géré
Certains avantages d'un environnement géré tel que
Microsoft® Common Language Runtime, comme la
sécurité d'écriture ou le fait que le logiciel
soit indépendant de la plate-forme, deviennent des
inconvénients dans un environnement en temps réel. En
général, vous ne pouvez pas vous permettre d'attendre
que le compilateur juste-à-temps (JIT) compile une
méthode avant de l'utiliser, ou encore que le Garbage
Collector efface la mémoire précédemment
allouée en éliminant les ressources non
utilisées. Ces deux fonctionnalités peuvent
interférer avec un comportement système
déterministe. Il est possible d'obliger le Garbage
Collector à effectuer son travail en appelant GC.Collect(). Cependant, du fait que le
Garbage Collector est fortement optimisé, il est
préférable de le laisser travailler de façon
autonome. Pour permettre un comportement en temps réel
« dur », il serait utile de disposer d'un
moyen de distinguer les fonctionnalités en temps réel
dur, écrites en code natif ou en code Microsoft
Win32® non géré, des autres fonctionnalités
écrites en code géré. C'est ce que vous permet
de faire Platform Invoke ou P/Invoke.
La fonction Platform Invoke
Selon l'aide MSDN®, P/Invoke est la fonctionnalité fournie par le Common Language Runtime qui permet à du code géré d'appeler les points d'entrée d'une bibliothèque de liaison dynamique (DLL) native non gérée. En d'autres termes, P/Invoke vous permet d'échapper au code géré de Microsoft .NET et d'opter pour du code non géré Win32. Pour que vous puissiez utiliser ce mécanisme dans Microsoft Windows® CE .NET, les fonctions Win32 natives que vous souhaitez appeler doivent être définies comme des fonctions publiques externes au sein d'une DLL. Comme l'environnement .NET géré ignore tout de la décomposition des noms (name mangling) C++, les fonctions à appeler à partir d'une application gérée doivent aussi respecter les conventions de désignation C. Pour pouvoir utiliser la fonctionnalité depuis une DLL, il vous faut créer une classe wrapper autour des points d'entrée de la fonction à partir de votre application gérée. L'extrait 1 illustre un exemple de petite DLL non gérée. L'extrait 2 explique comment appeler cette DLL à partir de code géré. Comme ce mécanisme fonctionne pour toutes les fonctions DLL exportées et que presque toutes les API Win32 sont exportées dans coredll.dll, ce mécanisme permet aussi d'effectuer des appels à presque toutes les API Win32. Dans notre test, nous avons utilisé P/Invoke pour que l'application gérée lance un appel dans une thread en temps réel non gérée.
// This is the function GetTimingInfo that exists in the
// unmanaged Win32 DLL. The function is fed with information,
// originating in an Interrupt Service Thread in the same
// DLL. On request of the managed application, timing
// information is copied using a double buffering mechanism.
RTCF_API DWORD GetTimingInfo(LPDWORD lpdwAvgPerfTicks,
LPDWORD lpdwMax,
LPDWORD lpdwMin,
LPDWORD lpdwDeltaMax,
LPDWORD lpdwDeltaMin)
{
g_bRequestData = TRUE;
if (WaitForSingleObject(g_hNewDataEvent,
1000)==WAIT_OBJECT_0)
{
*lpdwAvgPerfTicks = g_dwBufferedAvgPerfTicks;
*lpdwMax = g_dwBufferedMax;
*lpdwMin = g_dwBufferedMin;
*lpdwDeltaMax = g_dwBufferedDeltaMax;
*lpdwDeltaMin = g_dwBufferedDeltaMin;
return 1;
}
else
return 0;
}
// GetTimingInfo prototype
#ifdef RTCF_EXPORTS
#define RTCF_API __declspec(dllexport)
#else
#define RTCF_API __declspec(dllimport)
#endif
extern "C"
{
RTCF_API BOOL Init();
RTCF_API BOOL DeInit();
RTCF_API DWORD GetTimingInfo(LPDWORD lpdwAvgPerfTicks,
LPDWORD lpdwMax,
LPDWORD lpdwMin,
LPDWORD lpdwDeltaMax,
LPDWORD lpdwDeltaMin);
}
Extrait 1. DLL Win32 appelée à partir de code géré
// Wrapper class to be able to P/Invoke into a DLL.
// Exported functions in the DLL are imported by this
// wrapper. Note the use of compiler attributes to identify
// the physical DLL that hosts the exported functions.
using System;
using System.Runtime.InteropServices;
namespace CFinRT
{
public class WCEThreadIntf
{
[DllImport("RTCF.dll")]
public static extern bool Init();
[DllImport("RTCF.dll")]
public static extern bool DeInit();
[DllImport("RTCF.Dll")]
public static extern uint GetTimingInfo(
ref uint perfAvg,
ref uint perfMax,
ref uint perfMin,
ref uint perfTickMax,
ref uint perfTickMin);
}
}
// Call an unmanaged function from within managed code
public void CollectValue()
{
if (WCEThreadIntf.GetTimingInfo(ref aveSleepTime,
ref maxSleepTime,
ref minSleepTime,
ref curMaxSleepTime,
ref curMinSleepTime) != 0)
{
curMaxSleepTime = (uint)(float)((curMaxSleepTime *
scaleValue) / 1.19318);
curMinSleepTime = (uint)(float)((curMinSleepTime *
scaleValue) / 1.19318);
aveSleepTime = (uint)(float)((aveSleepTime *
scaleValue) / 1.19318);
maxSleepTime = (uint)(float)((maxSleepTime *
scaleValue) / 1.19318);
minSleepTime = (uint)(float)((minSleepTime *
scaleValue) / 1.19318);
}
StoreValue();
counter = (counter + 1) % samplesInMinute;
}
Extrait 2. Appel de code non géré
Un scénario en temps réel
Un système doit disposer de fonctionnalités en temps réel dur pour pouvoir récupérer des informations provenant d'une source externe. Ces informations sont stockées dans le système et présentées à l'utilisateur sous une forme graphique. La figure 1 illustre un scénario possible permettant de gérer ce problème.
Une thread en temps réel résidant dans une DLL Win32 native reçoit une interruption d'une source externe. La thread traite l'interruption et stocke les informations pertinentes à présenter à l'utilisateur. Du côté droit, une thread d'interface utilisateur séparée, écrite en code géré, lit les informations précédemment stockées par la thread en temps réel. Les changements de contexte entre processus étant coûteux, mieux vaut que tout le système réside dans le même processus. Si vous séparez la fonctionnalité temps réel de la fonctionnalité d'interface utilisateur en plaçant la première dans une DLL et en procurant une interface entre cette DLL et les autres parties du système, vous atteindrez votre objectif : un seul processus qui gère toutes les parties du système. La communication entre la thread de l'interface utilisateur et celle en temps réel s'effectue en accédant au code Win32 natif via P/Invoke.
Le test en lui-même
Vous souhaitez que votre test soit représentatif mais
qu'il reste aussi simple que possible pour qu'il puisse
être reproduit facilement sur d'autres systèmes. Vous
pouvez pour cela télécharger le code source qui vous
permettra d'exécuter ce test vous-même. Pour
réaliser ce test, il faut pouvoir introduire des
interruptions dans le système et effectuer des sondages
pour mesurer les performances du système. Vous alimentez
le système en utilisant un signal de blocage, produit par
un générateur de signal. Bien sûr, le
système d'exploitation de Windows CE .NET doit
être capable d'héberger .NET Compact Framework. Dans
l'un de ses articles, Paul Yao indique les modules et
composants Windows CE .NET qui doivent être
présents pour permettre d'exécuter des applications
gérées. Voir
Microsoft .NET Compact Framework for Windows CE .NET (
). Le test n'a pas pour seul
objectif d'être représentatif et reproductible, ni de
trouver une source d'interruption adaptée.
L'extrait 3 montre comment connecter une interruption
physique à une thread de service d'interruption.
RTCF_API BOOL Init()
{
BOOL bRet = FALSE;
DWORD dwIRQ = IRQ; // in our case IRQ = 5
// Get a SysIntr for the specified IRQ
if (KernelIoControl(IOCTL_HAL_TRANSLATE_IRQ,
&dwIRQ,
sizeof(DWORD),
&g_dwSysIntr,
sizeof(DWORD),
NULL))
{
// create an event that will activate our IST
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (g_hEvent)
{
// Connect the interrupt to our event and
// create our Interrupt Service Thread.
// The actual IST is shown in listing 4
InterruptDisable(g_dwSysIntr);
if (InterruptInitialize(g_dwSysIntr,
g_hEvent, NULL, 0))
{
g_bFinish = FALSE;
g_hThread = CreateThread(NULL,
0,
IST,
NULL,
0,
NULL);
if (g_hThread)
{
bRet = TRUE;
}
else
{
InterruptDisable(g_dwSysIntr);
CloseHandle(g_hEvent);
g_hEvent = NULL;
}
}
}
}
return bRet;
}
Extrait 3. Comment connecter une interruption physique à une thread de service d'interruption.
Pour tester le comportement en temps réel d'une application utilisant du code géré et .NET Compact Framework, nous avons créé une plate-forme Windows CE .NET basée sur le kit SDK Standard. Nous avons aussi inclus dans la plate-forme la version RTM de .NET Compact Framework. Le système d'exploitation utilise un Geode GX1 fonctionnant à 300 MHz. Nous alimentons le système avec un signal de blocage, immédiatement connecté à la ligne IRQ5 du bus PC104 (broche 23). La fréquence du signal de blocage est de 10 kHz. Sur les flancs montants, une interruption est générée. L'interruption est traitée par une thread de service d'interruption (IST, Interrupt Service Thread). Dans l'IST, nous envoyons des impulsions de sondage au port parallèle de façon à visualiser un signal de sortie. Nous enregistrons également l'heure à laquelle l'IST a été activée grâce à l'API haute résolution QueryPerformanceCounter. Pour pouvoir mesurer les informations de minutage sur une longue période, nous enregistrons, outre la durée moyenne, la durée maximum et la durée minimum. Le laps de temps qui s'écoule entre l'occurrence de l'interruption et la vérification en sortie indique la latence IRQ - IST. Les informations de minutage collectées par l'horloge haute résolution indiquent quand l'IST est activée. Dans l'idéal, cette valeur doit être de 100 µs pour une fréquence d'interruption de 10 kHz. Toutes les informations de minutage sont transmises à l'interface utilisateur graphique à intervalles réguliers.
Comme .NET Compact Framework ne peut pas être utilisé dans des situations en temps réel dur comme celles décrites précédemment, nous avons décidé de ne l'utiliser que pour la présentation et de recourir à une DLL écrite avec Microsoft Visual C++® 4.0 incorporé pour toutes les fonctionnalités en temps réel. Pour la communication entre la DLL et l'interface utilisateur graphique (GUI, Graphical User Interface) .NET Compact Framework, nous utilisons un mécanisme de double tampon associé à P/Invoke. L'interface graphique, qui nécessite de nouvelles informations de minutage à intervalles réguliers, utilise un objet System.Threading.Timer. La DLL décide, en fonction de sa disponibilité, du moment opportun pour transmettre les informations à l'interface graphique. Cette dernière reste bloquée jusqu'à ce que les données soient prêtes. L'utilisateur peut sélectionner la fréquence d'actualisation des informations présentées dans l'interface graphique. Dans notre test, nous avons opté pour une fréquence d'actualisation de 50 ms.
Le pseudocode suivant explique le fonctionnement de l'IST et le mécanisme utilisé par l'interface graphique pour récupérer les informations stockées dans la DLL Win32 native.
Interrupt Service Thread:
Wait
On IRQ 5 send probe pulse to the parallel port
Measure time with QueryPerformanceCounter
Store measured time (min, max, current, average) locally
if (userInterfaceRequestsData) {
copy measured time information
reset statistic measure values
set dataReady event
userInterfaceRequestsData = false
}
Mise à jour périodique en code géré des données d'affichage :
disable timer // See pitfalls
call with P/Invoke into the DLL
// The following code is implemented in the DLL
userInterfaceRequestsData = true
wait for dataReady event
return measured values
draw measured values on the display, each time using new graphics objects
update marker // A running vertical bar on the display
enable timer
Lors du test, nous avons connecté un oscilloscope et imprimé au bout de dix minutes de test une capture de l'affichage graphique de l'oscilloscope et de Windows CE .NET. La figure 2 montre la latence d'interruption mesurée par l'oscilloscope. Dans le meilleur des cas, la latence est de 14 µs, dans le pire de cas, elle est de 54,4 µs, ce qui représente une instabilité de 40,4 µs. La figure 3 montre les données périodiques d'activation de l'IST. Cette figure est une capture d'écran de l'interface utilisateur réelle. Dans l'idéal, l'IST doit s'exécuter toutes les 100 µs, ce qui correspond au temps moyen utilisé lors de nos mesures (ligne bleue). Nous avons aussi mesuré les temps globaux minimum (en vert) et maximum (en rouge), en plus des temps minimum et maximum pour la période d'échantillonnage de 50 millisecondes (pavé représenté en blanc). L'écart obtenu pendant la période de test est limité à ±40 µs.
Les résultats
Nous avons effectué les mesures sur une plus longue période pour garantir que le Garbage Collector et le compilateur JIT seraient fréquemment actifs. Grâce à nos collègues de Microsoft, qui nous ont fourni une clé de registre pour les compteurs de performance, nous avons pu suivre le comportement de .NET Compact Framework. Cette clé permet d'activer plusieurs compteurs de performance de .NET Compact Framework. Nous avons utilisé ces informations de performance surtout pour vérifier le bon fonctionnement du compilateur JIT et du Garbage Collector. Elles nous ont également fourni des indications utiles sur le nombre d'objets utilisés pendant le test.
// Our periodic timer method in which we want to collect new
// data and refresh the screen
private void OnTimer(object source)
{
// Temporarily stop the timer, to prevent against
// a whole bunch of OnTimer calls to be invoked
if (theTimer != null)
{
theTimer.Change(Timeout.Infinite, dp.Interval);
}
Pen blackPen = new Pen(Color.Black);
Pen yellowPen = new Pen(Color.Yellow);
Graphics gfx = CreateGraphics();
td.SetTimePointer(dp.CurrentSample, gfx, blackPen);
for (int i = 0; i < dp.SamplesPerMeasure; i++)
{
td.ShowValue(dp.CurrentSample, dp[i], gfx, i);
}
dp.CollectValue();
td.SetTimePointer(dp.CurrentSample, gfx, yellowPen);
gfx.Dispose();
yellowPen.Dispose();
blackPen.Dispose();
// Restart the timer again for the next update
if (theTimer != null)
{
theTimer.Change(dp.Interval, dp.Interval);
}
}
Extrait 4. Gestion des messages de l'horloge dans un environnement géré
Comme le montre l'extrait 4, nous instancions plusieurs objets à chaque mise à jour périodique de l'écran. Ces objets (deux stylos et un objet graphique) sont créés chaque fois que l'écran est mis à jour. Les fonctions td.ShowValue et td.SetTimerPointer créent également des pinceaux. Comme la fonction td.SetTimerPointer est appelée deux fois à chaque mise à jour de l'écran, six objets sont créés au total chaque fois que l'écran est mis à jour. Étant donné que l'écran est mis à jour toutes les 50 ms, 120 objets sont créés chaque seconde. Après 10 minutes d'exécution, le nombre d'objets créés s'élève à 72 000. Tous ces objets peuvent être soumis à un nettoyage de la mémoire (garbage collection). Dans le tableau 1, le nombre d'objets alloués correspond approximativement à ces valeurs théoriques.
| Compteur | Valeur | n | moyenne | min | max |
|---|---|---|---|---|---|
| Temps d'exécution total du programme | 603752 | 0 | 0 | 0 | 0 |
| Nombre d'octets maximum alloués | 1115238 | 0 | 0 | 0 | 0 |
| Nombre d'objets alloués | 66898 | 0 | 0 | 0 | 0 |
| Nombre d'octets alloués | 1418216 | 66898 | 21 | 8 | 24020 |
| Nombre de nettoyages simples | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets en cours d'utilisation après un nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Temps passé en nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Nombre de nettoyages compacts | 1 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage compact | 652420 | 1 | 652420 | 652420 | 652420 |
| Nombre d'octets en cours d'utilisation après un nettoyage compact | 134020 | 1 | 134020 | 134020 | 134020 |
| Temps passé en nettoyage compact | 357 | 1 | 357 | 357 | 357 |
| Nombre de nettoyages complets | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage complet | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets en cours d'utilisation après un nettoyage complet | 0 | 0 | 0 | 0 | 0 |
| Temps passé en nettoyage complet | 0 | 0 | 0 | 0 | 0 |
| Nombre d'opérations de nettoyage GC ( garbage collection) générées par l'application | 0 | 0 | 0 | 0 | 0 |
| Temps de latence GC | 357 | 1 | 357 | 357 | 357 |
| Nombre d'octets compilés avec Jit | 14046 | 259 | 54 | 1 | 929 |
| Nombre d'octets natifs compilés avec Jit | 70636 | 259 | 272 | 35 | 3758 |
| Nombre de méthodes compilées avec Jit | 259 | 0 | 0 | 0 | 0 |
| Nombre d'octets éliminés | 0 | 0 | 0 | 0 | 0 |
| Nombre de méthodes éliminées | 0 | 0 | 0 | 0 | 0 |
| Nombre d'exceptions | 0 | 0 | 0 | 0 | 0 |
| Nombre d'appels | 3058607 | 0 | 0 | 0 | 0 |
| Nombre d'appels virtuels | 1409 | 0 | 0 | 0 | 0 |
| Nombre de demandes satisfaites par le cache d'appels virtuels | 1376 | 0 | 0 | 0 | 0 |
| Nombre d'appels de PInvoke | 176790 | 0 | 0 | 0 | 0 |
| Nombre total d'octets en cours d'utilisation après un nettoyage | 421462 | 1 | 421462 | 421462 | 421462 |
Tableau 1. Résultats des performances de .NET Compact Framework pour une durée d'exécution du test de cinq minutes
Nous avons inclus les résultats des compteurs de performance obtenus pour une durée d'exécution de 10 minutes et pour une durée d'exécution de 100 minutes. Ces données ont été enregistrées durant le test réel. Comme vous pouvez le constater, après 10 minutes d'exécution, l'opération de nettoyage de la mémoire s'est produite sans baisse significative des performances. Le tableau 2 montre les compteurs de performance pour une durée d'exécution d'environ 100 minutes. Cette durée d'exécution a permis la réalisation d'un nettoyage de la mémoire complet. Sur cette durée, seuls 461 499 objets ont été créés au lieu des 720 000 escomptés. Cela représente environ 35 % d'objets en moins que prévu. Cette différence est probablement due aux compteurs de performance qui, selon Microsoft, génèrent une altération des performances d'environ 30 % dans l'application gérée. Quoi qu'il en soit, comme le montre la figure 4, le comportement en temps réel du système n'a pas été affecté.
| Compteur | Valeur | n | moyenne | min | max |
|---|---|---|---|---|---|
| Démarrage du moteur d'exécution | 478 | 0 | 0 | 0 | 0 |
| Durée totale d'exécution du programme | 5844946 | 0 | 0 | 0 | 0 |
| Nombre d'octets maximum alloués | 1279678 | 0 | 0 | 0 | 0 |
| Nombre d'objets alloués | 461499 | 0 | 0 | 0 | 0 |
| Nombre d'octets alloués | 8975584 | 461499 | 19 | 8 | 24020 |
| Nombre de nettoyages simples | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Nombre d'octets en cours d'utilisation après un nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Temps passé en nettoyage simple | 0 | 0 | 0 | 0 | 0 |
| Nombre de nettoyages compacts | 11 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage compact | 8514912 | 11 | 774082 | 656456 | 786476 |
| Nombre d'octets en cours d'utilisation après un nettoyage compact | 1679656 | 11 | 152696 | 147320 | 153256 |
| Temps passé en nettoyage compact | 5395 | 0 | 490 | 436 | 542 |
| Nombre de nettoyages complets | 2 | 0 | 0 | 0 | 0 |
| Nombre d'octets libérés à chaque nettoyage complet | 397428 | 2 | 198714 | 1916 | 395512 |
| Nombre d'octets en cours d'utilisation après un nettoyage complet | 79924 | 2 | 39962 | 17328 | 62596 |
| Temps passé en nettoyage complet | 65 | 2 | 32 | 2 | 63 |
| Nombre d'opérations de nettoyage GC (garbage collection) générées par l'application | 0 | 0 | 0 | 0 | 0 |
| Temps de latence GC | 5460 | 13 | 420 | 2 | 542 |
| Nombre d'octets compilés avec Jit | 19143 | 356 | 53 | 1 | 929 |
| Nombre d'octets natifs compilés avec Jit | 95684 | 356 | 268 | 35 | 3758 |
| Nombre de méthodes compilées avec Jit | 356 | 0 | 0 | 0 | 0 |
| Nombre d'octets éliminés | 85304 | 326 | 261 | 35 | 3758 |
| Nombre de méthodes éliminées | 385 | 0 | 0 | 0 | 0 |
| Nombre d'exceptions | 0 | 0 | 0 | 0 | 0 |
| Nombre d'appels | 21778124 | 0 | 0 | 0 | 0 |
| Nombre d'appels virtuels | 1067 | 0 | 0 | 0 | 0 |
| Nombre de demandes satisfaites par le cache d'appels virtuels | 1029 | 0 | 0 | 0 | 0 |
| Nombre d'appels de PInvoke | 1996991 | 0 | 0 | 0 | 0 |
| Nombre total d'octets en cours d'utilisation après un nettoyage | 5632119 | 13 | 433239 | 84637 | 493054 |
Tableau 2. Résultats des performances de .NET Compact Framework pour une durée d'exécution du test de 100 minutes
La visionneuse de processus distants confirme le fait que le Garbage Collector et le compilateur JIT n'affectent pas le comportement en temps réel. La figure 5 illustre un vidage d'écran de la visionneuse de processus distants pour l'application gérée. Toutes les threads de l'application (sauf la thread en temps réel avec priorité 0) s'exécutent avec des priorités normales (251). Pendant nos mesures, le blocage du noyau ne s'est pas avéré nécessaire pour que le compilateur JIT et le Garbage Collector puissent réaliser leur travail.
Les pièges
Pendant le test, le fait d'accroître la fréquence
du signal de blocage a généré des résultats
inattendus dans l'application gérée. L'application a
bloqué le système de façon aléatoire,
surtout lorsque l'écran avait besoin d'être
fréquemment redessiné parce que certaines de ses
zones n'étaient pas valides. L'étude de ce
problème a révélé un comportement qui a
surpris les programmeurs Win32 les plus chevronnés. Dans
une application Win32 , l'utilisation d'une horloge aboutit
à un message WM_TIMER chaque fois
que l'horloge arrive à expiration. Cependant, dans la file
d'attente, les messages WM_TIMER sont
des messages à priorité basse, qui ne sont
envoyés que s'il n'y a pas d'autres messages dotés
d'une priorité plus élevée à traiter. Ce
comportement peut affecter la fiabilité de l'horloge,
d'autant que CreateTimer ne fournit pas
une horloge précise. Ce n'est pas un problème,
surtout si l'horloge est utilisée pour mettre à jour
une interface utilisateur graphique (GUI). Néanmoins, dans
l'application gérée, nous utilisons un objet
System.Threading.Timer pour créer une horloge.
Cette horloge appelle un délégué chaque fois
qu'elle arrive à expiration. Ce délégué est
appelé depuis une thread séparée résidant
dans un pool de threads. Si le système est trop
occupé à d'autres activités (par exemple le
retraçage de l'ensemble de l'écran), d'autres
délégués de l'horloge sont activés, chacun
dans une thread distincte, avant que les
délégués précédemment activés
n'en aient terminé. Cela peut aboutir à l'utilisation
de toutes les threads disponibles dans le pool et provoquer le
blocage du système. L'extrait 4 donne la solution
pour éviter ce comportement. Chaque fois qu'un
délégué de l'horloge est activé, nous
interrompons l'objet horloge en appelant la méthode
Change de l'objet Timer pour indiquer que nous ne
voulons pas obtenir le message d'horloge suivant tant que nous
n'aurons pas traité le message en cours. Cela peut
générer des intervalles d'horloge imprécis. Dans
notre cas l'horloge n'est utilisée que pour actualiser
l'écran : cette imprécision ne constitue pas un
problème.
La vérification des résultats
Pour pouvoir comparer les résultats de notre test aux
résultats standard obtenus dans les mêmes conditions,
nous avons également écrit une application Win32
appelant la même DLL et dotée de la
fonctionnalité en temps réel. L'application Win32 est
fonctionnellement identique à l'application
gérée. Elle fournit au système une interface
utilisateur graphique qui affiche les informations d'horloge
dans une fenêtre. Cette application affiche les
résultats de l'horloge à la réception des
messages WM_TIMER en utilisant tout
simplement les API Win32. Comme le montrent les figures 6
et 7, nous n'avons constaté aucune différence
significative dans les performances. À la figure 6,
la latence d'interruption est là encore mesurée
à l'aide d'un oscilloscope. Pour l'application Win32, la
latence est de 14,4 µs. Dans le pire des cas, la
latence est de 55,2 µs, ce qui représente une
instabilité de 40,8 µs. Ces résultats sont
identiques à ceux obtenus lors du test exécuté
avec l'application gérée .NET Compact Framework.
À la figure 7, les données périodiques s'affichent lors de l'activation de l'IST, là encore pour l'application Win32. Les résultats sont là aussi identiques à ceux obtenus avec une application gérée .NET Compact Framework. Vous pouvez télécharger les deux sources correspondant à l'application gérée et à l'application Win32.
Conclusion
Vous comprendrez qu'il n'est pas question d'utiliser .NET Compact Framework pour n'importe quel travail en temps réel. Ce que nous suggérons, c'est de l'utiliser comme couche de présentation. Dans un système de ce type, .NET Compact Framework peut « coexister pacifiquement » avec la fonctionnalité en temps réel, sans que cela affecte le comportement en temps réel de Windows CE .NET. Dans cet article, nous n'avons pas détaillé les capacités graphiques de .NET Compact Framework. Dans notre cas, nous n'avons trouvé aucune différence significative entre une application écrite entièrement en Win32 et une application en partie écrite en C# dans un environnement géré. Étant donné que .NET Compact Framework offre au programmeur une meilleure productivité et une grande richesse, il est très avantageux d'écrire les couches de présentation en code géré et d'utiliser du code non géré pour écrire les fonctionnalités en temps réel dur. Cette approche vous permet d'établir une distinction précise entre ces deux types de fonctionnalités.
Remerciements
Cela faisait longtemps que nous souhaitions tester l'utilité de .NET Compact Framework dans des scénarios en temps réel. Ce test n'a été possible que grâce à la coopération de personnes et d'entreprises qui nous ont fourni le matériel et les équipements de mesure appropriés. C'est pourquoi nous souhaitons remercier Willem Haring de Getronics pour son soutien, ses idées et son hospitalité pendant la réalisation du projet. Nous tenons aussi à remercier nos collègues de Delem pour nous avoir accueillis et nous avoir fourni l'équipement nécessaire à la réalisation des tests.
À propos des auteurs
Michel Verhagen travaille chez PTS Software aux Pays-Bas. Consultant Windows CE .NET, il compte quatre années d'expérience dans l'utilisation de Windows CE. Il est plus particulièrement spécialisé sur Platform Builder.
Maarten Struys travaille aussi pour PTS Software. Il est responsable du centre sur les compétences incorporées et en temps réel. Expert dans le développement Windows (CE), il utilise Windows CE depuis son lancement. Depuis 2000, Maarten travaille sur du code géré dans des environnements .NET. Il est aussi journaliste indépendant pour deux grands magazines néerlandais spécialisés dans le développement de systèmes embarqués. Il a récemment ouvert un site Web donnant des informations sur .NET en environnement embarqué.
Ressources supplémentaires
Pour plus d'informations sur CE .NET, veuillez consulter le site Web de Windows Embedded.
Pour obtenir la documentation en ligne et l'aide contextuelle de Windows CE .NET, veuillez consulter la documentation de Windows CE .NET (en anglais).
Pour plus d'informations sur Microsoft Visual Studio® .NET, veuillez consulter le site Web de Visual Studio.
Dernière mise à jour le mercredi 16 juillet 2003