Scheduler et threads en Win32

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Scheduler et threads en Win32

Post by Hydraxx »

Salut, :)

aujourd’hui on va voir un composant central du noyau Windows : le scheduler, c’est-à-dire le mécanisme qui décide quel thread s’exécute, sur quel processeur, dans quel ordre, et pendant combien de temps. C’est un sujet très important, parce qu’en pratique dès qu’un programme devient multithread, orienté performance, réactif, ou simplement un peu complexe, on finit par rencontrer directement les effets du scheduler.

Si on comprend mal le scheduler, on comprend mal :
  • pourquoi un thread ne tourne pas quand on l’attend
  • pourquoi une appli devient lente sous charge
  • pourquoi certains threads “prennent toute la machine”
  • pourquoi des blocages ou des famines CPU apparaissent
  • pourquoi les réglages de priorité peuvent parfois aider… ou empirer la situation
Ce cours va donc poser une vision claire du scheduling Windows, avec le vocabulaire du noyau, les notions importantes, et les API Win32 utiles pour agir depuis le user-mode.

1) Le modèle mental fondamental

La première idée à fixer, c’est celle-ci :

Code: Select all

Windows ne planifie pas les processus.
Windows planifie les threads.
Un processus est un conteneur : mémoire, handles, token, modules, heaps, etc.
Mais ce n’est jamais le processus lui-meme qui reçoit le CPU. Ce sont toujours ses threads.

En pratique, pour le scheduler, l’unité d’exécution réelle est donc le thread.

Les grands états que tu dois connaitre sont :

Code: Select all

Initialized
Ready
Standby
Running
Waiting
Transition
Terminated
Dans une vision simplifiée, on retient souvent surtout :

Code: Select all

Ready
Running
Waiting
Terminated
Mais au niveau noyau, il faut savoir qu’il existe d’autres états intermédiaires ou plus précis.

Résumé utile :
  • Ready : le thread peut tourner, il attend un CPU
  • Standby : il a été choisi pour tourner très bientôt sur un processeur donné
  • Running : il s’exécute réellement
  • Waiting : il attend un objet, un délai, une I/O, un event, un mutex, etc.
  • Transition : état intermédiaire, souvent lié à la mémoire ou à une préparation
  • Terminated : le thread a fini
2) Ce que fait réellement le scheduler

Le scheduler répond en permanence à plusieurs questions :
  • quel thread READY doit être choisi
  • sur quel CPU il faut l’exécuter
  • faut-il préempter le thread courant
  • combien de temps le thread peut garder le processeur
  • faut-il favoriser le cache CPU ou la répartition de charge
Autrement dit, le scheduler ne fait pas que “donner le CPU”. Il arbitre entre priorité, équilibre, localité processeur, quantum, affinité, groupes de processeurs, et événements système.

3) Priorités : base du modèle de scheduling

Le scheduling Windows repose très fortement sur les priorités.

Le niveau logique global va de :

Code: Select all

1 → 31
Traditionnellement on retient :
  • 1 à 15 : priorités variables / normales
  • 16 à 31 : priorités temps réel
Le niveau 0 existe dans l’architecture interne pour certains usages système très particuliers, mais pour le développement habituel on raisonne surtout de 1 à 31.

Règle fondamentale :

Code: Select all

un thread READY de priorité plus haute passe avant un thread READY de priorité plus basse
C’est le cœur du mécanisme.

4) Classe de priorité du processus

Le processus possède une classe de priorité. Cette classe influence la priorité de base de ses threads.

Les classes importantes sont :

Code: Select all

IDLE_PRIORITY_CLASS
BELOW_NORMAL_PRIORITY_CLASS
NORMAL_PRIORITY_CLASS
ABOVE_NORMAL_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
REALTIME_PRIORITY_CLASS
APIs importantes :

Code: Select all

BOOL SetPriorityClass(
    HANDLE hProcess,
    DWORD dwPriorityClass
);

DWORD GetPriorityClass(
    HANDLE hProcess
);
Ces APIs servent à modifier ou lire la classe de priorité du processus.

Il faut bien comprendre que la classe de priorité du processus ne “remplace” pas la priorité du thread. Elle sert de base au calcul de la priorité de base des threads de ce processus.

5) Priorité du thread

Chaque thread possède lui aussi une priorité relative au sein de la classe de priorité du processus.

Constantes les plus connues :

Code: Select all

THREAD_PRIORITY_IDLE
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_TIME_CRITICAL
APIs importantes :

Code: Select all

BOOL SetThreadPriority(
    HANDLE hThread,
    int nPriority
);

int GetThreadPriority(
    HANDLE hThread
);
Ces valeurs ne sont pas directement les niveaux 1..31, mais des valeurs relatives combinées avec la classe du processus pour obtenir la priorité de base réelle du thread.

6) Priorité de base et priorité dynamique

Il faut absolument distinguer deux notions :

Code: Select all

Base Priority
Dynamic Priority
La base priority est la priorité de départ du thread, issue de :
  • la classe de priorité du processus
  • la priorité relative du thread
La dynamic priority, elle, est la priorité effectivement utilisée par le scheduler pour beaucoup de threads variables.

C’est la priorité dynamique qui peut être temporairement boostée ou réduite par le système selon certains événements.

Règle importante :
  • pour les threads à priorité variable, le scheduler utilise une priorité dynamique
  • pour les threads en temps réel, le comportement est plus rigide et beaucoup plus dangereux si mal utilisé
7) Priority boost

Windows utilise des boosts temporaires de priorité pour améliorer la réactivité ou éviter certaines formes de famine.

Cas fréquents :
  • fin d’I/O
  • réveil d’un thread GUI
  • sortie d’un wait sur certains objets
  • éviter qu’un thread reste trop longtemps sans CPU
Le principe est simple :

Code: Select all

un thread qui se réveille peut recevoir temporairement une priorité plus favorable
Cela permet par exemple à un thread interactif ou à un thread réveillé par une I/O de répondre rapidement.

APIs utiles à connaitre :

Code: Select all

BOOL SetThreadPriorityBoost(
    HANDLE hThread,
    BOOL DisablePriorityBoost
);

BOOL GetThreadPriorityBoost(
    HANDLE hThread,
    PBOOL pDisablePriorityBoost
);
Mal nommée pour les débutants, cette API permet surtout de désactiver ou vérifier la désactivation du boost automatique sur un thread.

8) Ready queues

Le scheduler maintient des files de threads prêts à s’exécuter.

Vision classique :

Code: Select all

32 ready queues
1 par niveau de priorité
Le modèle simplifié est :

Code: Select all

on choisit le thread READY de plus haute priorité
Ensuite, parmi plusieurs threads de meme priorité, d’autres règles entrent en jeu, notamment le round robin.

Au niveau noyau, cette logique est liée aux structures internes du scheduler et des processeurs, notamment autour des processeurs logiques et de leurs structures de contrôle.

9) Round Robin

Quand plusieurs threads de meme priorité sont READY, Windows applique une logique de rotation.

Code: Select all

Round Robin scheduling
Cela veut dire que les threads de meme priorité se partagent le CPU par quantum.

Autrement dit :
  • un thread tourne
  • son quantum expire
  • il retourne READY si rien ne le bloque
  • un autre thread de meme priorité peut passer
Le round robin n’annule pas la notion de priorité. Il s’applique entre threads équivalents du point de vue de la priorité courante.

10) Quantum

Le quantum est la tranche de temps maximale qu’un thread peut garder avant qu’un changement de scheduling soit envisagé.

Le quantum n’est pas une constante universelle gravée dans le marbre. Il dépend de plusieurs facteurs historiques et de configuration système. Il a varié selon les éditions, les politiques serveur/client et certains réglages.

L’idée importante à retenir n’est pas la valeur exacte, mais le rôle :
  • le quantum limite la monopolisation du CPU
  • quand il expire, le thread peut être réinséré en file READY
  • un thread plus prioritaire peut évidemment préempter avant la fin du quantum
Schéma mental :

Code: Select all

thread tourne
↓
quantum expire
↓
retour en ready queue si toujours exécutable
11) Préemption

Le scheduler Windows est préemptif.

Cela signifie qu’un thread courant peut être interrompu si un thread plus prioritaire devient READY.

Code: Select all

Higher priority thread → preemption
C’est une règle fondamentale. Elle explique énormément de comportements visibles en pratique.

Exemple typique :
  • thread A tourne à une priorité moyenne
  • thread B, plus prioritaire, se réveille après une I/O
  • thread A est préempté
  • thread B prend le CPU
Cela améliore la réactivité, mais cela veut aussi dire qu’un mauvais usage des priorités peut complètement déséquilibrer une application.

12) Starvation

La starvation, ou famine CPU, se produit quand un thread reste prêt mais obtient trop rarement le processeur parce que d’autres plus prioritaires lui passent toujours devant.

Exemple typique :
  • un thread de faible priorité est READY
  • des threads plus prioritaires arrivent sans cesse
  • le thread faible priorité ne tourne presque jamais
C’est un risque réel quand on abuse des priorités hautes ou temps réel.

Les boosts dynamiques existent justement aussi pour réduire certains cas de starvation, mais ils ne réparent pas toutes les mauvaises conceptions.

13) Scheduling multiprocesseur

Sur une machine multiprocesseur, le problème devient plus riche.

Le scheduler doit alors raisonner non seulement en priorité, mais aussi en distribution entre cœurs et processeurs logiques.

Points importants :
  • chaque CPU logique possède son contexte de scheduling
  • le scheduler cherche à choisir le bon thread pour le bon CPU
  • il tient compte de la charge
  • il tient compte de l’affinité
  • il tient compte de la localité du cache
Le but n’est pas seulement d’exécuter un thread quelque part, mais de le faire de manière efficace.

14) Localité cache, migration et équilibrage

Un thread qui a déjà tourné sur un CPU donné peut bénéficier de la chaleur du cache de ce CPU. Le scheduler essaie donc souvent d’éviter des migrations inutiles.

Mais à l’inverse, il doit aussi équilibrer la charge entre processeurs.

Il y a donc toujours une tension entre :
  • localité processeur / cache affinity
  • équilibrage global de la charge
C’est une des raisons pour lesquelles le scheduling est beaucoup plus complexe qu’une simple “liste triée par priorité”.

15) Ideal Processor

Chaque thread peut avoir un processeur idéal, c’est-à-dire une préférence de placement.

APIs importantes :

Code: Select all

DWORD SetThreadIdealProcessor(
    HANDLE hThread,
    DWORD dwIdealProcessor
);

BOOL SetThreadIdealProcessorEx(
    HANDLE hThread,
    PPROCESSOR_NUMBER lpIdealProcessor,
    PPROCESSOR_NUMBER lpPreviousIdealProcessor
);

BOOL GetThreadIdealProcessorEx(
    HANDLE hThread,
    PPROCESSOR_NUMBER lpIdealProcessor
);
Le processeur idéal n’est pas une obligation absolue comme une hard affinity stricte. C’est une préférence utilisée par le scheduler.

Structure importante :

Code: Select all

PROCESSOR_NUMBER
Elle décrit un processeur par :
  • Group
  • Number
  • Reserved
Très utile dès qu’on dépasse les hypothèses simplistes d’une machine à petit nombre de CPU.

16) Hard Affinity

L’affinité dure limite les CPU sur lesquels un thread ou un processus a le droit de tourner.

APIs importantes :

Code: Select all

BOOL SetProcessAffinityMask(
    HANDLE hProcess,
    DWORD_PTR dwProcessAffinityMask
);

BOOL GetProcessAffinityMask(
    HANDLE hProcess,
    PDWORD_PTR lpProcessAffinityMask,
    PDWORD_PTR lpSystemAffinityMask
);

DWORD_PTR SetThreadAffinityMask(
    HANDLE hThread,
    DWORD_PTR dwThreadAffinityMask
);
Avec l’affinité, on ne parle plus seulement de préférence, mais de restriction réelle.

Il faut l’utiliser avec précaution, parce qu’une affinité mal pensée peut :
  • réduire les performances
  • nuire au load balancing
  • créer des goulots d’étranglement
17) Processor Groups

Sur les machines ayant beaucoup de CPU logiques, Windows utilise la notion de Processor Groups.

Règle générale :

Code: Select all

1 group = max 64 CPU logiques
Cela a un impact énorme dès qu’on veut écrire un code vraiment scalable sur grosses machines.

APIs importantes :

Code: Select all

BOOL GetProcessGroupAffinity(
    HANDLE hProcess,
    PUSHORT GroupCount,
    PUSHORT GroupArray
);

BOOL SetThreadGroupAffinity(
    HANDLE hThread,
    const GROUP_AFFINITY* GroupAffinity,
    PGROUP_AFFINITY PreviousGroupAffinity
);

BOOL GetThreadGroupAffinity(
    HANDLE hThread,
    PGROUP_AFFINITY GroupAffinity
);
Structure clé :

Code: Select all

GROUP_AFFINITY
Elle contient notamment :
  • Mask
  • Group
C’est l’une des structures essentielles pour raisonner sur les grosses topologies CPU sous Windows.

18) CPU Sets

Windows moderne propose aussi les CPU Sets, plus souples que certains anciens mécanismes d’affinité.

APIs importantes :

Code: Select all

GetSystemCpuSetInformation()
SetProcessDefaultCpuSets()
SetThreadSelectedCpuSets()
GetProcessDefaultCpuSets()
GetThreadSelectedCpuSets()
SetThreadSelectedCpuSetMasks()
Structure importante :

Code: Select all

SYSTEM_CPU_SET_INFORMATION
Les CPU Sets permettent de sélectionner plus finement les CPU préférés ou autorisés, dans un modèle plus moderne et souvent mieux intégré à l’ordonnanceur.

C’est très utile dans certains scénarios de performance, de conteneurisation, ou d’isolation logique.

19) Background Mode

Windows permet aussi de basculer un processus en mode background.

Exemple :

Code: Select all

SetPriorityClass(
    GetCurrentProcess(),
    PROCESS_MODE_BACKGROUND_BEGIN
);
Constantes importantes :

Code: Select all

PROCESS_MODE_BACKGROUND_BEGIN
PROCESS_MODE_BACKGROUND_END
THREAD_MODE_BACKGROUND_BEGIN
THREAD_MODE_BACKGROUND_END
APIs utiles :

Code: Select all

SetPriorityClass()
SetThreadPriority()
Le mode background ne modifie pas seulement la priorité CPU au sens brut. Il influence aussi d’autres aspects de scheduling et de consommation de ressources selon le contexte système.

C’est utile pour des tâches discrètes, peu urgentes, qui ne doivent pas gêner l’interactivité.

20) Suspend / Resume

Ces APIs permettent de suspendre ou reprendre l’exécution de threads, et parfois d’un processus entier via l’API native.

APIs importantes :

Code: Select all

DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);

NtSuspendProcess()
NtResumeProcess()
Il faut être prudent avec ces fonctions :
  • suspendre un thread peut figer un verrou détenu par ce thread
  • suspendre au mauvais moment peut provoquer des blocages logiques
  • NtSuspendProcess est puissant, mais plus brutal au niveau global
Ce sont des outils utiles pour certains cas système, debug ou outillage, mais pas un mécanisme de synchronisation “normal”.

21) Sleep, yield et cession volontaire du CPU

APIs importantes :

Code: Select all

Sleep(ms)
SleepEx(ms, alertable)
SwitchToThread()
YieldProcessor()
Rôle :
  • Sleep : le thread passe en attente pendant au moins la durée indiquée
  • Sleep(0) : cède le reste du quantum à un thread prêt de priorité appropriée
  • SwitchToThread : tente de céder le CPU à un autre thread prêt sur le processeur courant
  • YieldProcessor : hint très léger pour certaines boucles d’attente active
Il faut bien comprendre qu’un Sleep n’est pas une primitive de synchronisation. C’est un outil de temporisation ou de cession volontaire, mais pas une vraie coordination logique.

22) Waits et impact sur le scheduler

Une grande partie du scheduling réel tourne autour des waits.

Quand un thread appelle par exemple :
  • WaitForSingleObject
  • WaitForMultipleObjects
  • Sleep
  • MsgWaitForMultipleObjects
  • SignalObjectAndWait
il quitte l’état Running et passe en Waiting.

C’est un point fondamental :

Code: Select all

un thread en attente ne consomme pas de CPU
Cela explique pourquoi un programme bien conçu passe souvent beaucoup de temps dans des waits propres, plutot que dans des boucles actives inutiles.

23) Structures et notions noyau à connaitre

Même si en user-mode on manipule surtout des APIs, il faut connaitre quelques noms du noyau pour bien lire la doc avancée, WinDbg, Windows Internals ou certains outils.

Noms importants à connaitre :

Code: Select all

KTHREAD
ETHREAD
KPROCESS
EPROCESS
KPRCB
DISPATCHER_HEADER
GROUP_AFFINITY
PROCESSOR_NUMBER
SYSTEM_CPU_SET_INFORMATION
CONTEXT
Rôle général :
  • KTHREAD : structure noyau centrale du thread coté scheduler
  • ETHREAD : enveloppe exécutive autour du thread
  • KPROCESS / EPROCESS : structures liées au processus
  • KPRCB : contrôle local par processeur, très important dans le scheduling multiprocesseur
  • DISPATCHER_HEADER : base commune de nombreux objets de synchronisation
Tu ne manipules pas directement KTHREAD en Win32 user-mode, mais connaitre ces noms change vraiment la lecture du sujet.

24) APIs utiles autour de la topologie CPU

Pour comprendre et interroger la machine, plusieurs APIs valent la peine d’être connues :

Code: Select all

GetSystemInfo()
GetNativeSystemInfo()
GetLogicalProcessorInformation()
GetLogicalProcessorInformationEx()
GetSystemCpuSetInformation()
GetCurrentProcessorNumber()
GetCurrentProcessorNumberEx()
GetNumaHighestNodeNumber()
GetNumaProcessorNodeEx()
Structures importantes associées :

Code: Select all

SYSTEM_INFO
SYSTEM_LOGICAL_PROCESSOR_INFORMATION
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX
PROCESSOR_RELATIONSHIP
NUMA_NODE_RELATIONSHIP
CACHE_RELATIONSHIP
GROUP_RELATIONSHIP
PROCESSOR_NUMBER
Ces APIs permettent de comprendre la topologie réelle de la machine, ce qui devient très utile dès qu’on parle d’affinité, groupes ou optimisation CPU.

25) NUMA et scheduler

Sur certaines machines, la topologie mémoire est NUMA.
Cela signifie que tous les accès mémoire ne coûtent pas pareil selon le nœud NUMA auquel le CPU et la mémoire sont liés.

Le scheduler et l’OS essaient d’en tenir compte, surtout sur les grosses machines.

APIs utiles à connaitre de nom :

Code: Select all

GetNumaHighestNodeNumber()
GetNumaNodeProcessorMaskEx()
GetNumaProcessorNodeEx()
VirtualAllocExNuma()
SetThreadIdealProcessorEx()
Tu n’as pas besoin d’être expert NUMA dès le début, mais il faut au moins savoir que sur de grosses machines, la question n’est pas seulement “quel CPU ?” mais aussi “quelle localité mémoire ?”.

26) APIs utiles autour des priorités et infos thread

En complément des APIs déjà vues, il faut aussi connaitre :

Code: Select all

GetThreadPriority()
GetThreadPriorityBoost()
SetThreadPriorityBoost()
GetPriorityClass()
SetPriorityClass()
GetThreadTimes()
GetProcessTimes()
GetThreadIOPendingFlag()
SetThreadInformation()
GetThreadInformation()
QueryThreadCycleTime()
QueryProcessCycleTime()
Quelques remarques :
  • GetThreadTimes / GetProcessTimes donnent des temps noyau et user utiles pour le diagnostic
  • QueryThreadCycleTime / QueryProcessCycleTime sont utiles pour mesurer de l’activité CPU en cycles
  • SetThreadInformation / GetThreadInformation donnent accès à certaines options modernes selon la classe d’information
27) Temps réel : à manier avec beaucoup de prudence

Le niveau temps réel existe, mais il ne faut surtout pas le prendre à la légère.

On parle notamment de :
  • REALTIME_PRIORITY_CLASS
  • THREAD_PRIORITY_TIME_CRITICAL
  • niveaux 16 à 31
Un thread temps réel mal conçu peut :
  • affamer des threads normaux
  • rendre la machine peu réactive
  • bloquer des composants essentiels si des waits ou boucles sont mal pensés
En pratique, il faut éviter d’utiliser le temps réel sans raison très solide et sans comprendre parfaitement les conséquences.

28) Ce qu’il faut retenir sur les performances

Quand on observe des performances CPU étranges, plusieurs facteurs scheduler doivent venir à l’esprit :
  • priorité trop haute ou trop basse
  • quantum et round robin entre threads de meme niveau
  • préemption par des threads plus prioritaires
  • affinité mal réglée
  • migration trop fréquente entre CPU
  • starvation
  • trop de threads pour trop peu de cœurs
  • boucles actives au lieu de waits propres
Beaucoup de problèmes attribués à “Windows est lent” sont en réalité des problèmes de conception du modèle de threads.

29) Erreurs classiques

Parmi les erreurs fréquentes :
  • croire que Windows planifie les processus
  • abuser de SetThreadPriority ou REALTIME_PRIORITY_CLASS
  • mettre une hard affinity sans vraie raison
  • utiliser Sleep comme primitive de synchronisation
  • suspendre des threads sans réfléchir aux verrous qu’ils détiennent
  • ignorer les processor groups sur grosses machines
  • confondre processeur idéal et affinité dure
  • supposer qu’un thread “qui existe” a forcément du CPU
30) Résumé des APIs à connaitre absolument

Pour les priorités :

Code: Select all

GetPriorityClass
SetPriorityClass
GetThreadPriority
SetThreadPriority
GetThreadPriorityBoost
SetThreadPriorityBoost
Pour l’affinité et le placement :

Code: Select all

SetProcessAffinityMask
GetProcessAffinityMask
SetThreadAffinityMask
SetThreadIdealProcessor
SetThreadIdealProcessorEx
GetThreadIdealProcessorEx
GetCurrentProcessorNumber
GetCurrentProcessorNumberEx
Pour les processor groups et CPU sets :

Code: Select all

GetProcessGroupAffinity
SetThreadGroupAffinity
GetThreadGroupAffinity
GetSystemCpuSetInformation
SetProcessDefaultCpuSets
SetThreadSelectedCpuSets
Pour le contrôle d’exécution :

Code: Select all

SuspendThread
ResumeThread
NtSuspendProcess
NtResumeProcess
Sleep
SleepEx
SwitchToThread
YieldProcessor
Pour la topologie et le diagnostic :

Code: Select all

GetSystemInfo
GetNativeSystemInfo
GetLogicalProcessorInformation
GetLogicalProcessorInformationEx
GetThreadTimes
GetProcessTimes
QueryThreadCycleTime
QueryProcessCycleTime
31) Structures et types à connaitre absolument

Code: Select all

GROUP_AFFINITY
PROCESSOR_NUMBER
SYSTEM_INFO
SYSTEM_LOGICAL_PROCESSOR_INFORMATION
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX
PROCESSOR_RELATIONSHIP
GROUP_RELATIONSHIP
NUMA_NODE_RELATIONSHIP
CACHE_RELATIONSHIP
CONTEXT
KTHREAD
ETHREAD
KPRCB
32) Conclusion

Le scheduler Windows repose sur quelques idées simples à énoncer, mais très riches dans leurs conséquences :
  • Windows planifie les threads, pas les processus
  • la priorité est la base du choix
  • les ready queues organisent les threads prêts
  • le quantum limite la monopolisation du CPU
  • le round robin partage le CPU entre threads de meme priorité
  • la préemption donne immédiatement la main aux threads plus prioritaires
  • sur les machines modernes, affinité, groupes, cache et topologie comptent énormément
Comprendre le scheduler, c’est comprendre une grande partie du comportement réel des programmes multithread sous Windows.

Et surtout, ça permet de passer d’un raisonnement du type :

Code: Select all

"mon thread ne tourne pas, Windows bug"
à un raisonnement beaucoup plus juste :

Code: Select all

"quel est son état, sa priorité, son affinité, sa concurrence, son wait, son CPU cible, et qu’est-ce que le scheduler est en train de faire avec lui ?"
A la prochaine pour le prochain cours :)

Who is online

Users browsing this forum: No registered users and 0 guests