La fois précédente (partie 1), nous avons vu que les requêtes d’E/S de Windows se conditionnent en un paquet appelé IRP et descendent la pile de périphériques, et que l’émission et l’achèvement sont, tout au fond du noyau, séparés dès le départ.
Cette fois, nous creusons le mécanisme qui permet à une application d’exploiter cette séparation depuis son propre côté — l’E/S asynchrone (E/S overlapped). On ajoute FILE_FLAG_OVERLAPPED et pourtant l’appel revient de façon synchrone. On réutilise une structure OVERLAPPED et les données se corrompent. On appelle CancelIoEx et rien ne s’arrête. On annule et le programme plante avec une violation d’accès — toutes ces « histoires qui font peur » de l’E/S asynchrone viennent du même endroit : ne pas avoir le mécanisme en tête comme une image unique. Cet article construit cette image.
C’est la deuxième partie de la série « Les profondeurs de l’I/O Windows ». Le plan d’ensemble se trouve au début de la partie 1.
1. La conclusion, d’abord
- La frontière entre E/S synchrone et E/S asynchrone n’est pas dans le noyau, mais dans le fait d’« attendre ou non ». La garantie de l’E/S synchrone est que « l’appel ne revient pas avant l’achèvement ». Ce n’est que lorsque la requête passe en attente que le gestionnaire d’E/S attend l’achèvement, et le thread s’endort dans un état d’attente qui ne consomme pas de CPU (chapitre 2).1
FILE_FLAG_OVERLAPPEDest un mode du handle (de l’objet fichier). Il est fixé dèsCreateFileet ne peut pas être changé d’un appel à l’autre. Pour un handle asynchrone, le système ne gère pas le pointeur de fichier, donc pour un fichier sur disque, la position doit être indiquée à chaque fois viaOffsetdansOVERLAPPED(chapitre 3).21- La structure
OVERLAPPEDest « un bordereau pour une seule opération ». Il en faut autant que d’opérations en cours d’émission, et la documentation officielle précise que le partage ou la réutilisation entraîne une corruption de données. Il ne faut toucher ni à la structure ni au tampon avant l’achèvement (chapitre 3).34 - Il y a, dans les faits, quatre façons de recevoir l’achèvement. La signalisation du handle (déconseillée), l’événement de
OVERLAPPEDcombiné àGetOverlappedResult, l’APC (attente alertable), et le port d’achèvement d’E/S (la prochaine fois) (chapitre 4).156 - Une émission asynchrone peut malgré tout s’achever de façon synchrone. Les données déjà en cache, la compression/le chiffrement NTFS, une écriture qui allonge le fichier en sont les exemples typiques. « Asynchrone » ne veut pas dire « ne bloque jamais » (chapitre 5).3
- L’annulation est une « demande ». Même après avoir appelé
CancelIoEx, l’opération revient sous forme d’un achèvement avecERROR_OPERATION_ABORTED. Il ne faut faire aucun nettoyage avant d’avoir constaté cet achèvement (chapitre 6).78 - Le
FileOptions.Asynchronousde .NET est le commutateur direct de ce mode. Un décalage entre le mode du handle et l’API appelée produit un « asynchrone de façade » pris en charge par le pool de threads, ou une attente inutile (chapitre 7).910
2. E/S synchrone — où le thread s’endort-il ?
Commençons par la forme par défaut. Un handle ouvert sans FILE_FLAG_OVERLAPPED est en mode synchrone. Quand on appelle ReadFile, la fonction ne revient pas avant que l’E/S ne soit achevée.1
Comme nous l’avons vu dans la partie 1, le pilote met la requête en attente (pending) et attend la réponse du matériel. Alors, en E/S synchrone, qui attend ? C’est le gestionnaire d’E/S qui attend l’achèvement avant de rendre la main à l’application.
sequenceDiagram
participant App as Thread de l'application
participant IOM as Gestionnaire d'E/S
participant DRV as Pilote (pile)
App->>IOM: ReadFile(handle synchrone)
IOM->>DRV: Émet l'IRP
DRV-->>IOM: STATUS_PENDING (en attente de réponse)
Note over App,IOM: Le thread passe en état d'attente dans le noyau<br/>et s'endort sans consommer de CPU
DRV->>IOM: Achèvement (IoCompleteRequest)
IOM-->>App: Retourne le résultat et réveille le thread<br/>ReadFile revient avec TRUE/FALSE
Figure 1 : E/S synchrone (quand la requête passe en attente). Le gestionnaire d’E/S attend l’achèvement avant de revenir vers l’application
Notez que ce schéma correspond au cas où la requête passe en attente. Si le pilote peut compléter la requête sur-le-champ (succès de cache, par exemple — le chemin de « complétion immédiate » de la figure 5 de la partie 1), aucune attente ne survient nulle part, et le résultat revient directement. La garantie de l’E/S synchrone est que « l’appel ne revient pas avant l’achèvement », pas qu’« on s’endort forcément ».
Deux points méritent d’être retenus.
- « Attendre » ne consomme pas de CPU. Un thread en état d’attente sort du champ d’exécution du planificateur. Nous avons expliqué pourquoi il vaut mieux laisser attendre un thread plutôt que de s’étrangler soi-même avec du polling, dans « Pourquoi préférer l’attente sur événement à Sleep(1) sous Windows ».
- Pour un handle en mode synchrone, le noyau gère le pointeur de fichier (la position courante). C’est pour cela que des
ReadFilesuccessifs peuvent lire « à la suite ». Cet état vit dans l’objet fichier, pas dans le handle, donc un handle dupliqué avecDuplicateHandlepartage cette position (partie 1, section 3.3).
La faiblesse de l’E/S synchrone tient entièrement au fait que le thread ne peut rien faire d’autre pendant qu’il attend. Faire de l’E/S synchrone sur le thread d’interface fige l’écran, et créer un thread par connexion sur un serveur finit par en accumuler des centaines dès quelques centaines de connexions. Il existe par ailleurs une API, CancelSynchronousIo, pour secourir de l’extérieur un autre thread bloqué dans une E/S synchrone (chapitre 6).11
3. E/S asynchrone — le mode du handle et le bordereau d’une opération
3.1. Le mode se décide au niveau du handle
Passer FILE_FLAG_OVERLAPPED à CreateFile ouvre l’objet fichier derrière ce handle en mode asynchrone.1 Ce qui compte ici, c’est que c’est une propriété du handle. On ne peut pas dire « asynchrone seulement pour cet appel ». On peut en revanche ouvrir le même fichier avec deux handles distincts, l’un synchrone et l’autre asynchrone (cela crée simplement deux objets fichier).
Un handle en mode asynchrone présente une autre différence majeure : le système ne gère pas le pointeur de fichier.2 Quand plusieurs opérations sont en vol simultanément, la notion de « position courante » n’a plus de sens. Pour un périphérique doté d’une position, comme un fichier sur disque, la position de lecture ou d’écriture doit être explicitée à chaque fois via Offset/OffsetHigh de la structure OVERLAPPED. À l’inverse, pour un périphérique sans notion de position de recherche, comme un port série ou un tube nommé, Offset n’est pas utilisé pour indiquer une position (on le laisse à 0). Même dans ce cas, comme nous le verrons dans la section suivante, la structure OVERLAPPED elle-même reste nécessaire pour chaque opération.
3.2. OVERLAPPED est « un bordereau pour une seule opération »
Le rôle de la structure OVERLAPPED est d’identifier une opération en cours d’émission et de transporter son état.4
| Membre | Rôle |
|---|---|
Offset / OffsetHigh |
La position dans le fichier où cette opération lit ou écrit (indiquée à l’émission ; inutilisée pour un périphérique sans position) |
hEvent |
L’événement signalé à l’achèvement (facultatif, réinitialisation manuelle recommandée) |
Internal |
L’état de l’opération. Avant achèvement, contient l’équivalent de STATUS_PENDING (usage système) |
InternalHigh |
Le nombre d’octets transférés à l’achèvement (usage système) |
flowchart LR
subgraph H["Handle (objet fichier) = mode"]
M1["Mode synchrone<br/>Le noyau gère la position courante<br/>ReadFile ne revient pas avant l'achèvement"]
M2["Mode asynchrone (FILE_FLAG_OVERLAPPED)<br/>La position courante n'est pas gérée<br/>L'émission et l'achèvement sont séparés"]
end
subgraph O["Structure OVERLAPPED = bordereau d'une opération"]
F1["Offset : où lire"]
F2["hEvent : comment être notifié de l'achèvement"]
F3["Internal/InternalHigh :<br/>état et résultat (écrits par le système)"]
end
C["Se décide une seule fois, à CreateFile"] --> H
R["Une instance préparée à chaque émission de ReadFile/WriteFile"] --> O
Figure 2 : le mode appartient au handle, l’état à l’opération (le bordereau). Confondre cette répartition mène à des accidents
De là découlent naturellement deux interdictions que la documentation officielle formule explicitement.3
- Il faut autant de structures
OVERLAPPEDque d’opérations en vol simultanément. Trois E/S émises exigent trois structures. Les réutiliser mène à « des résultats imprévisibles ou une corruption de données ». - Jusqu’à l’achèvement, il faut garder à la fois
OVERLAPPEDet le tampon de données vivants, sans y toucher. Le noyau vient écrire dans cette zone. Émettre une opération avec unOVERLAPPEDen variable locale, puis sortir de la fonction, est l’accident classique où l’on fait piétiner sa propre pile par le noyau.
3.3. Trois façons dont l’émission peut se dérouler
Un ReadFile sur un handle asynchrone peut revenir de trois façons.2
flowchart TB
A["ReadFile(handle asynchrone, avec OVERLAPPED)"]
Q{"Quelle est la valeur de retour ?"}
T["TRUE<br/>Achevé sur-le-champ (achèvement synchrone)<br/>Par défaut, une notification d'achèvement arrive aussi séparément"]
P["FALSE + ERROR_IO_PENDING<br/>Accepté. L'achèvement sera notifié plus tard"]
E["FALSE + une autre erreur<br/>L'émission elle-même a échoué"]
W["Attendre la notification d'achèvement<br/>(les quatre méthodes du chapitre 4)"]
A --> Q
Q --> T
Q --> P
Q --> E
P --> W
Figure 3 : les trois branches de l’émission asynchrone. L’E/S asynchrone ne fonctionne vraiment que si l’on gère correctement à la fois TRUE (achèvement immédiat) et ERROR_IO_PENDING
Dans le code, la décision se fait selon la combinaison de la valeur de retour et de GetLastError. Ce tableau se traduit directement en branchement.
Valeur de retour de ReadFile |
GetLastError() |
Signification | Ce que doit faire l’appelant |
|---|---|---|---|
TRUE |
(à ignorer) | Achevé sur-le-champ (achèvement synchrone) | Par défaut, une notification d’achèvement arrive aussi séparément. Laisser le traitement du résultat à la notification |
FALSE |
ERROR_IO_PENDING (997) |
Accepté. En cours | Ne rien faire. Attendre la notification d’achèvement sans toucher ni à OVERLAPPED ni au tampon |
FALSE |
Autre chose | L’émission elle-même a échoué | La notification d’achèvement n’arrivera pas. Traiter l’erreur sur-le-champ et nettoyer OVERLAPPED et le tampon |
// C++ / Win32
// hFile : handle ouvert avec FILE_FLAG_OVERLAPPED
// ov : OVERLAPPED alloué spécifiquement pour cette opération (Offset et hEvent déjà configurés)
// buf/len: tampon dédié à cette opération. Ne pas le libérer avant d'avoir reçu la notification d'achèvement
DWORD IssueRead(HANDLE hFile, OVERLAPPED* ov, BYTE* buf, DWORD len)
{
// Pour une émission asynchrone, on passe NULL à lpNumberOfBytesRead ;
// le nombre d'octets transférés est récupéré via GetOverlappedResult après l'achèvement
if (ReadFile(hFile, buf, len, nullptr, ov))
{
// (1) Achèvement synchrone. Par défaut la notification arrive aussi, donc on ne traite pas le résultat ici
return ERROR_SUCCESS;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// (2) Accepté. On attend la notification d'achèvement sans toucher à ov ni à buf
return ERROR_IO_PENDING;
}
// (3) Échec de l'émission elle-même. La notification n'arrivera pas, donc l'appelant nettoie ici
return err;
}
ERROR_IO_PENDING n’est pas une erreur, c’est une « acceptation ». Les deux bugs classiques sont de le traiter comme une erreur ordinaire, ou à l’inverse d’écrire du code sans prévoir le cas TRUE (achèvement synchrone). Le chapitre 5 explique pourquoi un achèvement synchrone peut se produire.
Il y a ici une remarque importante à ajouter. Par défaut, une notification d’achèvement arrive aussi, séparément, même pour une opération achevée de façon synchrone (TRUE). Pour un handle associé à un port d’achèvement d’E/S, un paquet d’achèvement est mis en file ; avec la méthode par événement, l’événement est également signalé. Écrire « si TRUE, traiter le résultat sur-le-champ, puis le traiter encore quand la notification arrive » mène donc à l’accident où la même opération est traitée deux fois et son bordereau libéré deux fois. La forme de base sûre consiste à centraliser le traitement du résultat du côté de la notification, pour les deux chemins « TRUE (achèvement synchrone) » et « ERROR_IO_PENDING ». Il existe cependant une troisième voie — quand l’émission elle-même échoue (FALSE + une autre erreur), aucune notification d’achèvement n’arrive. Renvoyer ce cas vers l’attente de notification revient à attendre indéfiniment ; c’est donc au point d’émission de traiter l’erreur et de nettoyer le bordereau sur-le-champ. Ce n’est que si l’on souhaite basculer vers « traiter sur-le-champ en sautant la notification lors d’un achèvement synchrone » qu’on active explicitement SetFileCompletionNotificationModes (FILE_SKIP_COMPLETION_PORT_ON_SUCCESS) — mais cela ne supprime que le paquet vers le port d’achèvement d’E/S, pas la signalisation de OVERLAPPED.hEvent. C’est une optimisation réservée à la voie IOCP, inutilisable avec la méthode par événement (chapitre 5).12
Notez que si l’on passe un OVERLAPPED à un handle en mode synchrone, la lecture se fait bien à partir de la position Offset, mais le comportement bloquant jusqu’à l’achèvement ne change pas.2 Ce n’est pas « passer un OVERLAPPED qui rend l’opération asynchrone » — le mode reste, en définitive, une propriété du handle.
4. Comment savoir qu’une E/S est achevée — quatre voies de notification
Puisque l’émission et l’achèvement sont séparés, la manière de recevoir le signal « c’est achevé » devient le cœur de la conception. Il y a, dans les faits, quatre voies.1
flowchart TB
DONE["L'E/S s'achève dans le noyau<br/>(IoCompleteRequest -> résultat fixé via un APC)"]
N1["(1) Le handle de fichier passe à l'état signalé<br/>Réception : WaitForSingleObject(handle)"]
N2["(2) Le hEvent de OVERLAPPED passe à l'état signalé<br/>Réception : WaitForSingleObject + GetOverlappedResult"]
N3["(3) La routine d'achèvement est placée dans la file d'APC du thread émetteur<br/>Réception : exécutée pendant une attente alertable comme SleepEx"]
N4["(4) Un paquet d'achèvement entre dans le port d'achèvement d'E/S<br/>Réception : GetQueuedCompletionStatus (partie 3)"]
DONE --> N1
DONE --> N2
DONE --> N3
DONE --> N4
Figure 4 : les quatre voies de notification d’achèvement. La voie utilisée dépend de la façon dont l’opération a été émise (présence de hEvent, ReadFileEx, association au port)
D’abord, une vue d’ensemble sous forme de tableau. Chaque section détaille ensuite son contenu.
| Méthode | Thread où s’exécute le traitement d’achèvement | Nombre d’E/S simultanées possibles | Cas d’usage adapté |
|---|---|---|---|
| (1) Signalisation du handle | N’importe quel thread en attente | Une seule, dans les faits. Impossible de distinguer laquelle est achevée si plusieurs sont émises | Presque aucun (4.1) |
(2) Événement + GetOverlappedResult |
N’importe quel thread en attente | Un événement par opération. Avec WaitForMultipleObjects pour attendre en groupe, la limite est de 64 |
Quelques E/S simultanées. Communication avec un périphérique (4.2) |
(3) APC (ReadFileEx) |
Le thread émetteur, et seulement pendant qu’il est en attente alertable | Aucune limite de nombre, mais tout le traitement d’achèvement s’exécute en série sur ce seul thread | Traitement de communication que l’on veut boucler sur un seul thread (4.3) |
| (4) Port d’achèvement d’E/S | Le groupe de threads de travail associé au port | Un grand nombre pris en charge par peu de threads | Serveurs, pools de threads (4.4) |
4.1. La signalisation du handle — à ne pas utiliser
Si l’on émet sans renseigner hEvent, c’est le handle de fichier lui-même qui passe à l’état signalé à l’achèvement. Cela semble pratique à première vue, mais si plusieurs opérations sont en vol sur le même handle, on ne peut pas distinguer laquelle est achevée.1 Sauf cas particulier où l’on n’émet jamais qu’une seule E/S asynchrone à la fois, il est plus sûr de ne pas l’utiliser.
4.2. Événement + GetOverlappedResult — la forme de base
On émet en renseignant un événement à réinitialisation manuelle dans OVERLAPPED.hEvent, on attend avec WaitForSingleObject (ou WaitForMultipleObjects pour plusieurs à la fois), puis on récupère le résultat (succès et nombre d’octets transférés) avec GetOverlappedResult.13 En passant TRUE au paramètre bWait de GetOverlappedResult, on peut aussi « attendre l’achèvement puis récupérer le résultat » en un seul appel. Si l’événement est à réinitialisation automatique, un autre point d’attente peut consommer le signal, et GetOverlappedResult reste alors bloqué indéfiniment — c’est pourquoi la réinitialisation manuelle est recommandée.134
C’est la méthode la plus lisible pour gérer solidement quelques E/S simultanées. Pour un périphérique où « lire tout en écrivant » est indispensable, comme un port série, cette forme reste d’actualité aujourd’hui encore (voir « Pièges des applications de communication série — de la reconnexion à la conception des journaux »).
4.3. APC — livré au thread qui a émis l’opération
ReadFileEx/WriteFileEx reçoivent, à la place d’un événement, une routine d’achèvement (fonction de rappel). À l’achèvement, cette routine est placée dans la file d’APC du thread émetteur, et elle s’exécute quand le thread entre dans une attente alertable telle que SleepEx ou WaitForSingleObjectEx.14515
Le trait distinctif de cette méthode est que « le traitement d’achèvement s’exécute toujours sur le thread émetteur ». Cela dispense de verrouillage, mais tant que le thread émetteur n’entre pas en attente alertable, la routine d’achèvement ne s’exécute jamais. Combiner cela avec la boucle de messages d’un thread d’interface exige MsgWaitForMultipleObjectsEx, ce qui complique la conception de l’attente — pour un usage général, on choisit plus souvent l’événement ou l’IOCP.
Et « l’APC n’arrive jamais » est le bug classique de cette méthode. La cause est presque toujours unique : l’attente n’est pas alertable.
// C++ / Win32. hFile est un handle ouvert avec FILE_FLAG_OVERLAPPED,
// on suppose que ov et buf restent vivants jusqu'à l'achèvement (section 3.2)
// Mauvais exemple : la routine d'achèvement n'est jamais appelée
ReadFileEx(hFile, buf, len, ov, OnReadCompleted);
Sleep(1000); // Attente non alertable. L'APC n'est jamais livré
// Bon exemple : rester en attente alertable jusqu'à ce que cette E/S se termine
//
// Ce drapeau est levé côté routine d'achèvement (par exemple porté par une structure englobant ov)
volatile bool completed = false;
// Toujours vérifier que l'émission a réussi. Si 0 est retourné, aucune routine d'achèvement n'a été mise en file
if (!ReadFileEx(hFile, buf, len, ov, OnReadCompleted))
{
const DWORD err = GetLastError(); // À récupérer immédiatement : un autre appel API l'écraserait
ReportError(err); // Périphérique débranché, handle invalide, etc.
return; // ★ Ne pas entrer dans la boucle d'attente ci-dessous
}
while (!completed)
{
DWORD r = SleepEx(1000, TRUE); // Le TRUE du deuxième argument rend l'attente alertable
if (r == WAIT_IO_COMPLETION)
{
// Un APC quelconque s'est exécuté. Rien ne garantit que ce soit notre propre E/S,
// donc on vérifie completed et on réattend si ce n'est pas le cas
continue;
}
// Retour par expiration du délai. L'E/S est toujours en vol,
// donc pour l'interrompre, on annule avec CancelIoEx et on attend que l'achèvement soit livré
CancelIoEx(hFile, ov);
}
N’entrez jamais dans la boucle d’attente sans avoir regardé la valeur de retour de ReadFileEx. Si l’émission elle-même échoue — juste après le débranchement d’un appareil, un handle déjà invalide, etc. — ReadFileEx retourne 0, et aucune routine d’achèvement n’est mise en file. Entrer dans while (!completed) dans cet état fait que completed ne se lève jamais, et l’on obtient une boucle qui répète indéfiniment SleepEx et CancelIoEx pour une E/S qui n’existe pas. Comme cela ressemble en apparence à « l’appareil ne répond simplement pas », remonter à la cause prend du temps. Si 0 est retourné, récupérez GetLastError() sur-le-champ (un seul autre appel d’API entre les deux suffit à l’écraser), et sortez sans entrer dans l’attente.
Un seul appel à SleepEx ne suffit pas. Un retour par expiration du délai fait sortir le thread de l’attente alertable à cet instant précis. L’E/S est toujours en vol, donc si ov ou buf disparaissent ensuite en sortant de la portée, le noyau vient piétiner un tampon qu’il croit toujours vivant (section 3.2). Faites l’une des deux choses suivantes : enregistrer l’achèvement côté routine d’achèvement et continuer à attendre jusqu’à ce qu’il se lève, ou annuler avec CancelIoEx puis attendre que l’achèvement soit livré.
Que WAIT_IO_COMPLETION soit retourné signifie seulement « au moins un APC s’est exécuté » — rien ne garantit que ce soit la routine d’achèvement de notre propre E/S. Si une autre E/S ou un APC de QueueUserAPC est mis en file sur le même thread, c’est celui-là qui peut provoquer le retour. Il ne faut donc jamais juger sur la seule valeur de retour, mais regarder le drapeau que l’on a soi-même défini.
Notez que le choix de la fonction d’attente elle-même reste simple : remplacez Sleep par SleepEx(..., TRUE), et WaitForSingleObject par WaitForSingleObjectEx(..., TRUE). Si vous avez écrit une routine d’achèvement et que rien ne se passe, vérifiez d’abord que la fonction d’attente se termine bien par Ex, et que l’argument alertable vaut bien TRUE.5
4.4. Le port d’achèvement d’E/S — la vraie solution qui passe à l’échelle (la prochaine fois)
Le mécanisme qui permet de recevoir un grand nombre d’E/S simultanées avec peu de threads est le port d’achèvement d’E/S (IOCP). En associant un handle au port, les achèvements sont mis dans la file du port, et les threads de travail les retirent avec GetQueuedCompletionStatus.6 C’est aussi, en dernier ressort, l’endroit où aboutit l’E/S de l’async/await de .NET. Nous y consacrerons entièrement la prochaine partie.
5. Le problème de « censé être asynchrone, mais achevé de façon synchrone »
C’est le premier écueil dans la conception d’une E/S asynchrone. Même émise correctement en mode asynchrone, il est parfaitement normal qu’une E/S s’achève de façon synchrone. Microsoft en cite les raisons typiques dans un document de dépannage.3
flowchart TB
A["Émission de ReadFile/WriteFile sur un handle asynchrone"]
Q{"L'une des conditions d'achèvement synchrone s'applique-t-elle ?"}
C1["Une requête satisfiable immédiatement<br/>(données déjà en cache, etc.)"]
C2["Fichier compressé NTFS<br/>(un fichier compressé ne devient jamais asynchrone)"]
C3["Fichier chiffré NTFS (EFS)"]
C4["Écriture qui allonge la taille du fichier"]
T["Retour immédiat avec TRUE<br/>= exécuté jusqu'à l'achèvement à l'intérieur de l'appel"]
P["Retour avec ERROR_IO_PENDING<br/>= véritablement en cours de façon asynchrone"]
A --> Q
Q --> C1
Q --> C2
Q --> C3
Q --> C4
C1 --> T
C2 --> T
C3 --> T
C4 --> T
Q -->|"Aucune de ces conditions"| P
Figure 5 : les principales conditions d’un achèvement synchrone. Le cache, la compression, le chiffrement et l’écriture qui allonge le fichier « ne deviennent jamais asynchrones »
Chacune a sa propre raison.3
- Un succès de cache. De nombreux pilotes ont un traitement spécial : « une requête satisfiable immédiatement est complétée sur-le-champ ». Pour un disque, cela survient quand les données sont déjà dans le cache en mémoire. C’est rapide, donc en soi ce n’est pas un problème — mais du code qui présuppose que « ERROR_IO_PENDING revient toujours » casse précisément ici.
- À l’inverse, il y a aussi un piège quand les données ne sont pas en cache. Le cache de Windows est implémenté par mappage de fichier, et comme le traitement des défauts de page en l’absence de page n’a pas de mécanisme asynchrone, une lecture asynchrone avec cache activé peut être traitée de façon synchrone. Le mécanisme du cache lui-même sera traité dans la partie 4.
- Compression NTFS, chiffrement EFS. Le pilote de système de fichiers convertit en synchrone l’accès aux fichiers compressés ou chiffrés.
- Une écriture qui allonge le fichier. Une écriture qui modifie la taille du fichier devient synchrone.
L’implication pratique est simple.
- Écrivez toujours le chemin « retour immédiat avec TRUE ». Les trois branches de la figure 3 sont toutes des cas normaux. Toutefois, comme une notification d’achèvement arrive aussi séparément par défaut même pour un achèvement synchrone, il est plus sûr de centraliser le traitement du résultat lui-même du côté de la notification (section 3.3).
- Cela ne peut pas servir de garantie de réactivité. L’idée que « l’interface ne se fige pas parce que c’est asynchrone » ne tient pas. Sur un thread qui ne doit jamais se figer, il faut concevoir dès le départ pour ne pas y émettre d’E/S (en séparant vers un thread dédié ou un pool de threads). Nous avons aussi traité ce côté pratique dans « Guide pratique pour se rapprocher autant que possible du temps réel souple sur un Windows ordinaire ».
- Pour des E/S à haute fréquence, l’achèvement synchrone est aussi une occasion d’optimiser. Il existe une API,
SetFileCompletionNotificationModes, qui permet d’omettre le paquet vers le port d’achèvement d’E/S lors d’un achèvement synchrone, et elle porte ses fruits combinée à l’IOCP (partie 3).12
6. Annulation et nettoyage — « arrête-toi » est une demande
Quand on veut arrêter une E/S qui prend longtemps (une destination réseau qui ne répond pas, des données série qui n’arrivent jamais), la bonne approche est CancelIoEx.7
sequenceDiagram
participant App as Application
participant IOM as Gestionnaire d'E/S
participant DRV as Pilote
App->>IOM: CancelIoEx(handle, OVERLAPPED)
Note over IOM: Demande l'annulation (marque)<br/>l'IRP non achevé correspondant
IOM->>DRV: Appelle la routine d'annulation
Note over DRV: Interrompt si l'état le permet<br/>peut s'achever normalement si déjà proche de la fin
DRV->>IOM: IoCompleteRequest<br/>(STATUS_CANCELLED)
IOM-->>App: La notification d'achèvement arrive<br/>GetOverlappedResult retourne ERROR_OPERATION_ABORTED
Note over App: Ne libérer OVERLAPPED et le tampon<br/>qu'après avoir constaté cette notification
Figure 6 : le déroulement réel de l’annulation. Même une opération annulée revient sous forme d’« achèvement »
Une fois le mécanisme connu, trois conséquences deviennent évidentes.
- L’annulation est une « demande » asynchrone. Même si
CancelIoExréussit, cela signifie seulement qu’elle a « marqué » l’opération. Une opération déjà proche de son achèvement peut malgré tout se terminer normalement.8 - Une opération annulée revient elle aussi via la notification d’achèvement, sous la forme de
ERROR_OPERATION_ABORTED. Jusqu’à la réception de cette notification,OVERLAPPEDet le tampon restent en cours d’utilisation par le noyau. Les libérer avant provoque une corruption de mémoire. C’est presque toujours la cause de « ça s’est mis à planter depuis qu’on annule ».78 - Réglez le sort des E/S non achevées avant de fermer le handle. Comme nous l’avons vu dans la partie 1, la fermeture du dernier handle déclenche l’annulation des IRP non achevés lors du traitement de cleanup, mais du code qui « ferme seulement le handle en laissant des E/S déjà émises en suspens » finit souvent par perdre le contrôle de la notification d’achèvement et de la durée de vie du tampon. Le principe est l’ordre annuler → constater l’achèvement → fermer.
Deux précisions en complément. L’ancien CancelIo ne peut annuler que les opérations émises par le thread appelant lui-même (une limitation datant d’avant l’arrivée de CancelIoEx sous Vista ; il n’y a aujourd’hui aucune raison de l’utiliser délibérément).16 Par ailleurs, pour un autre thread bloqué dans une E/S synchrone, on utilise CancelSynchronousIo.11 « Le système d’exploitation ne s’occupe pas du délai d’expiration à votre place — c’est à vous de concevoir l’annulation. » Voilà le cœur de la pratique de l’E/S asynchrone.
7. Vu depuis .NET — un décalage de mode produit un « asynchrone de façade »
Tout ce qui précède se relie directement au code .NET. Le paramètre useAsync du constructeur de FileStream (ou FileOptions.Asynchronous) est exactement le commutateur direct de FILE_FLAG_OVERLAPPED (voir le tableau de correspondance de la partie 1).
flowchart TB
A["await fs.ReadAsync(...)"]
Q{"Le handle est-il en mode asynchrone<br/>(FileOptions.Asynchronous) ?"}
Y["Véritable E/S asynchrone<br/>Émet l'équivalent d'un OVERLAPPED<br/>L'achèvement passe par l'IOCP vers le pool de threads (partie 3)"]
N["Asynchrone de façade<br/>Un thread du pool de threads<br/>prend en charge un Read synchrone et attend"]
A --> Q
Q -->|Oui| Y
Q -->|Non| N
Figure 7 : pour un même ReadAsync, ce qui se passe en dessous est totalement différent selon le mode du handle
La différence tient à une seule ligne, celle qui ouvre le fichier. Le code qui appelle ReadAsync reste identique, si bien qu’on ne peut pas s’en rendre compte à la simple lecture du code.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
string path = @"C:\temp\data.bin";
byte[] buffer = new byte[4096];
// (A) Asynchrone de façade. Omettre useAsync ou le mettre à false ouvre le handle en mode synchrone
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: false))
{
// L'appelant n'est pas bloqué, mais en coulisses un thread du pool prend en charge un Read synchrone et attend
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (B) Véritable asynchrone. useAsync: true se relie directement à FILE_FLAG_OVERLAPPED
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true))
{
// L'achèvement passe par l'IOCP vers le pool de threads (partie 3)
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (C) .NET 6 et versions ultérieures. Une écriture directe où le mode et le décalage sont explicites
using (SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read,
options: FileOptions.Asynchronous))
{
int read = await RandomAccess.ReadAsync(handle, buffer, fileOffset: 0);
}
La différence entre (A) et (B) tient au seul mot useAsync (écrire FileOptions.Asynchronous revient au même) — c’est exactement la condition qui reproduit l’« asynchrone de façade ». Pour auditer du code existant, ne cherchez pas du côté de ReadAsync/WriteAsync, mais là où FileStream est construit. Les surcharges courtes comme File.OpenRead ou new FileStream(path, FileMode.Open) ouvrent toutes en mode synchrone. Notez aussi que, pour construire un FileStream à partir d’un SafeFileHandle, l’argument isAsync doit correspondre au mode réel du handle.
- Un handle en mode synchrone combiné à
ReadAsyncest un « asynchrone de façade » où la lecture synchrone se fait sur un thread du pool de threads. L’appelant n’attend pas, mais un thread dort en coulisses. En petit nombre, le mal réel reste limité, mais sur un serveur ou un traitement à haute fréquence, cela devient une cause d’épuisement du pool de threads.9 - Un handle en mode asynchrone combiné à un
Readsynchrone est aussi une incohérence, dans le sens inverse, avec un gaspillage dû à l’attente interne de l’achèvement. Le principe est d’accorder le mode et l’API appelée.10 - À partir de .NET 6, l’implémentation interne de
FileStreama été entièrement réécrite, et une nouvelle API est apparue :File.OpenHandle+RandomAccess, qui permet de « lire et écrire en explicitantSafeFileHandleet le décalage ».9 Cette forme, où l’on passe le décalage à chaque fois, est exactement le visage brut du Win32 vu dans cet article — un handle asynchrone accompagné deOVERLAPPED.Offset. - Pour un handle en mode asynchrone, l’annulation d’une E/S de fichier via
CancellationTokenaboutit en interne àCancelIoEx. Derrière unReadAsyncauquel on a passé un tel jeton, qui se termine par uneOperationCanceledException, c’est exactement le schéma du chapitre 6 qui fonctionne. Le fait que l’annulation reste une « demande », sans immédiateté garantie, s’applique de la même façon. En revanche, dans l’« asynchrone de façade » d’un handle en mode synchrone, il n’existe pas d’opération overlapped à annuler, donc cette voie est inutilisable. Les runtimes .NET récents incluent bien un mécanisme qui tente d’annuler, viaCancelSynchronousIo, un tel appel en cours d’exécution synchrone, mais son efficacité dépend de la version du runtime et du type d’opération, et une interruption certaine n’est pas garantie. Si vous voulez faire de l’annulation un principe de conception, la bonne approche reste d’aligner les modes pour obtenir une véritable E/S asynchrone.
Notez que pour le côté pratique plus en surface de la façon d’écrire async/await (ConfigureAwait, la relation avec le thread d’interface), reportez-vous à « Tableau de décision pratique pour C# async/await - Task.Run et ConfigureAwait » et à « WPF/WinForms : async et le thread UI récapitulés en une fiche ». Cet article en est le premier sous-sol ; la prochaine partie (IOCP) en sera le second.
8. Résumé
- L’E/S synchrone et l’E/S asynchrone ne sont pas deux plomberies distinctes, mais une différence entre le gestionnaire d’E/S qui attend l’achèvement, ou qui revient sans attendre. Le thread d’une E/S synchrone s’endort en état d’attente et ne consomme pas de CPU.1
- Le mode appartient au handle (l’objet fichier), l’état appartient à l’opération (
OVERLAPPED). Un handle asynchrone ne gère pas de pointeur de fichier, donc pour un fichier doté d’une position, il faut l’indiquer à chaque fois viaOffset.24 OVERLAPPEDet le tampon doivent rester vivants et intacts jusqu’à la notification d’achèvement. Il en faut autant que d’émissions simultanées. Les réutiliser cause une corruption de données.3- La notification d’achèvement emprunte quatre voies : handle, événement, APC, IOCP. Pour plusieurs E/S simultanées, n’utilisez pas la signalisation du handle ; utilisez un événement à réinitialisation manuelle ; l’APC suppose une attente alertable.1135
- Même émise de façon asynchrone, une E/S s’achève de façon synchrone pour le cache, la compression/le chiffrement NTFS ou une écriture qui allonge le fichier. Écrivez toujours le chemin « retour immédiat avec TRUE » comme un cas normal, et ne vous en servez jamais comme garantie de réactivité.3
- L’annulation est une demande. Même après
CancelIoEx, ne nettoyez qu’après avoir constaté la notification d’achèvement (ERROR_OPERATION_ABORTED). L’ordre est : annuler → constater l’achèvement → fermer.78 - Le
FileOptions.Asynchronousde .NET est le relais direct deFILE_FLAG_OVERLAPPED: un décalage entre le mode et l’API produit un « asynchrone de façade ». Sous le capot deCancellationToken, c’estCancelIoExqui fonctionne.910
La suite est la partie 3, « Les ports d’achèvement d’E/S (IOCP) et le pool de threads .NET — le sous-sol d’async/await ». Nous y descendons jusqu’à comprendre pourquoi l’IOCP, dont nous n’avons fait que citer le nom à la section 4.4, unifie dans sa conception « la file de notifications d’achèvement » et « le contrôle du nombre de threads d’exécution », et sur quel thread s’exécute la suite d’un await.
Articles connexes
- Les profondeurs de l’I/O Windows (partie 1) — Chaque lecture et écriture devient un IRP : la vue d’ensemble du système d’E/S
- Tableau de décision pratique pour C# async/await - Task.Run et ConfigureAwait
- WPF/WinForms : async et le thread UI récapitulés en une fiche
- Pièges des applications de communication série — de la reconnexion à la conception des journaux
- Pourquoi préférer l’attente sur événement à Sleep(1) sous Windows
- Guide pratique pour se rapprocher autant que possible du temps réel souple sur un Windows ordinaire
- Le malentendu selon lequel TCP renvoie les données dans les mêmes unités que Send — concevoir la réception comme un flux d’octets
Domaines de conseil associés
合同会社小村ソフト (Komura Software LLC) prend en charge la conception d’applications métier Windows et d’applications de communication avec des périphériques utilisant l’E/S asynchrone, ainsi que l’investigation des causes de dysfonctionnements tels que « ça se fige », « ça plante lors d’une annulation » ou « le pool de threads s’épuise ».
- Développement d’applications Windows
- Investigation de bugs et analyse des causes
- Développement d’applications Windows à temps réel souple
- Contact
Références
-
Microsoft Learn, Synchronous and asynchronous I/O. Sur le fait qu’en E/S synchrone, la fonction bloque jusqu’à l’achèvement de l’E/S, tandis qu’en E/S asynchrone, la fonction ayant émis la requête revient immédiatement et le thread peut poursuivre d’autres travaux ; sur la nécessité d’ouvrir le handle avec FILE_FLAG_OVERLAPPED pour l’E/S asynchrone ; sur les méthodes de notification d’achèvement — signalisation du handle de fichier, signalisation de l’événement indiqué dans la structure OVERLAPPED, routine d’achèvement (APC) exécutée pendant une attente alertable, port d’achèvement d’E/S ; et sur le fait que, lorsque plusieurs opérations sont émises simultanément, la signalisation du handle de fichier ne permet pas de distinguer quelle opération est achevée. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, ReadFile function. Sur le fait que lpOverlapped est obligatoire pour un handle ouvert avec FILE_FLAG_OVERLAPPED, et que la position de début de lecture est indiquée via Offset/OffsetHigh de la structure OVERLAPPED ; sur le fait qu’en cas de traitement asynchrone, FALSE et ERROR_IO_PENDING sont retournés ; sur le fait que le système ne maintient pas le pointeur de fichier pour un handle asynchrone ; et sur le fait que, pour un handle ouvert sans FILE_FLAG_OVERLAPPED auquel on passe un OVERLAPPED, la lecture se fait à partir du décalage indiqué mais ReadFile ne revient pas avant l’achèvement de la lecture. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Sur les raisons pour lesquelles une E/S codée pour être asynchrone s’achève malgré tout de façon synchrone : un fichier compressé NTFS (le pilote de système de fichiers n’accède pas aux fichiers compressés de façon asynchrone, toutes les opérations devenant synchrones), un fichier chiffré NTFS, une écriture qui allonge la taille du fichier, une requête satisfiable immédiatement (par exemple quand les données sont déjà dans le cache en mémoire), auquel cas le pilote complète l’opération sur-le-champ et retourne TRUE ; sur le fait que le cache de Windows est implémenté par mappage de fichier et qu’il n’existe pas de mécanisme asynchrone pour le traitement des défauts de page en l’absence de page ; et, en complément, sur le fait que si l’on émet 3 E/S il faut 3 structures OVERLAPPED, leur réutilisation menant à des résultats imprévisibles ou une corruption de données, et sur le fait qu’il ne faut pas lire ni écrire dans le tampon de données correspondant tant que l’opération n’est pas achevée. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, OVERLAPPED structure. Sur le fait que la structure OVERLAPPED porte les informations nécessaires à l’entrée-sortie asynchrone ; sur le fait qu’Offset/OffsetHigh portent la position dans le fichier, hEvent l’événement signalé à l’achèvement, et Internal/InternalHigh le code d’état de l’opération et le nombre d’octets transférés ; sur le fait qu’il ne faut pas modifier la structure pendant l’exécution de l’opération et qu’elle doit rester valide ; et sur les précautions liées à l’utilisation d’un événement. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Alertable I/O. Sur le fait qu’en E/S alertable, l’entrée vers la routine d’achèvement est placée dans la file d’APC du thread ; sur le fait que l’APC s’exécute lorsque le thread entre en état alertable via SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, etc. ; et sur le fait que l’APC s’exécute toujours dans le contexte du thread émetteur. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, I/O Completion Ports. Sur le fait que le port d’achèvement d’E/S fournit un modèle de threading efficace pour traiter un grand nombre de requêtes d’E/S asynchrones sur un système multiprocesseur ; sur le fait qu’associer un handle de fichier au port fait entrer les paquets d’achèvement dans une file, que les threads de travail retirent avec GetQueuedCompletionStatus ; et sur le fait que le port contrôle le nombre de threads exécutés en concurrence. ↩ ↩2
-
Microsoft Learn, CancelIoEx function. Sur le fait que CancelIoEx marque pour annulation les E/S non achevées d’un handle donné, quel que soit le thread qui les a émises ; sur le fait qu’indiquer lpOverlapped ne cible que cette opération, tandis que NULL cible toutes les E/S non achevées ; sur le fait qu’une opération annulée s’achève avec ERROR_OPERATION_ABORTED ; et sur le fait que l’annulation de toutes les opérations n’est pas garantie, et qu’il faut attendre que le traitement d’achèvement soit terminé. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Canceling pending I/O operations. Sur le mécanisme d’annulation des E/S non achevées, et sur le fait que même après une demande d’annulation, l’opération peut déjà être en train de s’achever ; sur le fait qu’il faut confirmer l’achèvement d’une opération annulée avant de libérer les ressources ; et sur la répartition entre CancelSynchronousIo pour les opérations synchrones et CancelIo/CancelIoEx pour les opérations asynchrones. ↩ ↩2 ↩3 ↩4
-
Microsoft .NET Blog, File IO improvements in .NET 6. Sur le fait que l’implémentation interne de FileStream a été entièrement réécrite dans .NET 6 ; sur le fait que la stratégie diffère selon que le handle est ouvert en mode asynchrone ou non ; sur le fait que File.OpenHandle permet d’obtenir directement un SafeFileHandle, et que RandomAccess permet une lecture/écriture avec décalage explicite (thread-safe) ; et sur le fait qu’un appel asynchrone sur un handle non asynchrone est déchargé vers le pool de threads. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Asynchronous file I/O (.NET). Sur la conception de l’E/S de fichier asynchrone en .NET, sur le fait que pour utiliser l’E/S asynchrone avec FileStream il faut indiquer useAsync (FileOptions.Asynchronous) au constructeur pour activer l’E/S asynchrone au niveau de l’OS, et sur le choix entre méthodes synchrones et asynchrones. ↩ ↩2 ↩3
-
Microsoft Learn, CancelSynchronousIo function. Sur le fait que CancelSynchronousIo marque pour annulation une opération d’E/S synchrone en cours d’exécution sur un thread donné ; et sur le fait qu’une opération annulée revient en échec avec ERROR_OPERATION_ABORTED. ↩ ↩2
-
Microsoft Learn, SetFileCompletionNotificationModes function. Sur le fait que FILE_SKIP_COMPLETION_PORT_ON_SUCCESS permet de choisir de ne pas mettre de paquet d’achèvement dans la file du port d’achèvement d’E/S quand une E/S réussit immédiatement ; et sur le fait que FILE_SKIP_SET_EVENT_ON_HANDLE permet d’omettre la signalisation de l’événement du handle de fichier. ↩ ↩2
-
Microsoft Learn, GetOverlappedResult function. Sur le fait que GetOverlappedResult récupère le résultat d’une opération asynchrone (succès et nombre d’octets transférés) ; sur le fait que passer TRUE à bWait fait attendre l’achèvement de l’opération ; et sur le fait que, si hEvent de OVERLAPPED est un événement à réinitialisation automatique et qu’une autre attente en a consommé le signal, un appel avec bWait=TRUE risque de ne pas détecter l’achèvement et de rester bloqué, d’où la recommandation d’utiliser un événement à réinitialisation manuelle. ↩ ↩2 ↩3
-
Microsoft Learn, ReadFileEx function. Sur le fait que ReadFileEx reçoit une routine d’achèvement (FileIOCompletionRoutine) appelée à l’achèvement de la lecture ; sur le fait que la routine d’achèvement s’exécute lorsque le thread appelant est en état d’attente alertable ; et sur la nécessité d’un handle ouvert avec FILE_FLAG_OVERLAPPED. ↩
-
Microsoft Learn, Asynchronous Procedure Calls. Sur le fait qu’un APC est une fonction exécutée de façon asynchrone dans le contexte d’un thread particulier ; sur le fait que chaque thread possède sa propre file d’APC ; et sur le fait qu’un APC en mode utilisateur ne s’exécute que lorsque le thread est en état alertable. ↩
-
Microsoft Learn, CancelIo function. Sur le fait que CancelIo ne peut annuler que les opérations d’E/S émises par le thread appelant lui-même ; et sur le fait qu’il faut utiliser CancelIoEx pour annuler aussi des opérations émises par d’autres threads. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Les profondeurs de l'E/S Windows (épisode 4) — Le gestionnaire de cache : quand votre WriteFile atteint-il vraiment le disque ?
Quatrième épisode d'une série qui explique en images le gestionnaire de cache de Windows. Cet article détaille le cache implémenté comme ...
Les profondeurs de l'I/O Windows (partie 3) — Les ports d'achèvement d'E/S (IOCP) et le pool de threads .NET : le sous-sol d'async/await
Troisième volet d'une série qui explique en schémas le port d'achèvement d'E/S (IOCP). Nous y détaillons la conception qui unifie la file...
Les profondeurs de l'I/O Windows (partie 1) — Chaque lecture et écriture devient un IRP : la vue d'ensemble du système d'E/S
Premier volet d'une série qui explique le système d'E/S de Windows en partant de la base. Nous cartographions l'espace de noms de l'Objec...
Les profondeurs de l'I/O Windows (partie 6, dernière partie) — Pilotes filtres et minifiltres : pourquoi Procmon et les antivirus peuvent intercepter vos E/S
Dernier volet d'une série qui explique à l'aide de schémas les pilotes de filtre et les minifiltres de Windows. Nous y détaillons le Filt...
Les profondeurs de l'I/O Windows (partie 5) — Structure interne de NTFS : comprendre le système de fichiers à travers la MFT
Cinquième volet d'une série qui explique la structure interne de NTFS à l'aide de schémas. Nous y présentons la MFT et les enregistrement...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Qu'est-ce qui change quand on ajoute FILE_FLAG_OVERLAPPED ?
- L'objet fichier derrière le handle est ouvert en « mode asynchrone ». C'est une propriété du handle, fixée dès l'appel à CreateFile, et il est impossible de basculer entre synchrone et asynchrone d'un appel à l'autre. Pour un handle en mode asynchrone, il faut toujours passer une structure OVERLAPPED à ReadFile/WriteFile. Le système ne gère pas de pointeur de fichier (position courante) pour ce handle, donc pour un périphérique doté d'une position comme un fichier sur disque, la position de lecture ou d'écriture doit être indiquée à chaque fois via le champ Offset de OVERLAPPED (pour un périphérique sans position, comme un port série, Offset n'est pas utilisé). Une opération émise peut rendre la main avant son achèvement : dans ce cas, ReadFile retourne FALSE et GetLastError vaut ERROR_IO_PENDING. L'achèvement est reçu via une notification — événement, APC, port d'achèvement d'E/S, etc.
- Pourquoi une E/S émise de façon asynchrone revient-elle parfois immédiatement, déjà achevée ?
- Parce que le mode asynchrone signifie « on n'est pas obligé d'attendre l'achèvement », pas « on n'attend jamais ». La documentation Microsoft cite comme raisons typiques pour lesquelles une E/S émise de façon asynchrone se complète malgré tout de façon synchrone : une requête qui peut être satisfaite immédiatement (par exemple quand les données sont déjà dans le cache), un fichier compressé NTFS, un fichier chiffré NTFS (EFS), ou une écriture qui allonge la taille du fichier. Dans ces cas, ReadFile/WriteFile retourne TRUE et le résultat est déjà déterminé sur place. Le code qui utilise l'E/S asynchrone doit donc toujours prévoir à la fois le cas « retour avec ERROR_IO_PENDING » et le cas « achèvement immédiat », et la réactivité n'est jamais garantie de façon absolue pour autant. Notez que, par défaut, une notification d'achèvement (signalisation de l'événement ou paquet vers le port d'achèvement d'E/S) arrive aussi séparément pour les opérations achevées de façon synchrone : il est donc plus sûr de centraliser le traitement du résultat du côté de cette notification.
- Peut-on réutiliser une même structure OVERLAPPED pour plusieurs opérations ?
- Il ne faut jamais la partager entre plusieurs opérations en cours simultanément. La structure OVERLAPPED représente « l'état d'une seule opération en cours d'émission » ; la documentation Microsoft précise elle-même que si vous émettez 3 E/S, il vous faut 3 structures OVERLAPPED, et que les réutiliser entraîne des résultats imprévisibles ou une corruption de données. Tant que l'opération n'est pas achevée, la structure comme le tampon de lecture/écriture doivent rester valides et intacts — n'y touchez pas. Si vous la réutilisez après achèvement, réinitialisez-la à chaque fois pour éviter que des données résiduelles de la fois précédente n'interfèrent. Pour hEvent, il est plus sûr d'utiliser un événement à réinitialisation manuelle.
- Comment annuler en cours de route une E/S en train de s'exécuter ?
- CancelIoEx permet de demander l'annulation des E/S non achevées d'un handle donné, quel que soit le thread qui les a émises. En passant une structure OVERLAPPED en deuxième argument, seule cette opération précise est ciblée ; avec NULL, toutes les opérations de ce handle le sont. L'ancien CancelIo, lui, ne peut annuler que « les opérations émises par le thread appelant lui-même ». Le point important est que l'annulation est une « demande », pas une « garantie » immédiate : une opération déjà proche de son achèvement peut se terminer normalement malgré tout, et une opération effectivement annulée est notifiée comme achevée avec ERROR_OPERATION_ABORTED. Dans les deux cas, il ne faut libérer ni la structure OVERLAPPED ni le tampon avant d'avoir reçu la notification d'achèvement. Pour un autre thread bloqué dans une E/S synchrone, il existe une API dédiée : CancelSynchronousIo.
- Que se passe-t-il si l'on ne spécifie pas FileOptions.Asynchronous (useAsync) sur un FileStream .NET ?
- Le handle est ouvert en mode synchrone, donc même en appelant ReadAsync/WriteAsync, on n'obtient pas une véritable E/S asynchrone, mais un « asynchrone de façade » où un thread du pool de threads prend en charge la lecture/écriture synchrone à votre place. Le thread appelant n'est pas bloqué, mais un autre thread attend en coulisses, ce qui peut provoquer un épuisement du pool de threads et une baisse de la scalabilité. À l'inverse, ouvrir en mode asynchrone puis appeler des Read/Write synchrones entraîne un surcoût interne d'attente de l'achèvement. Le principe est d'accorder « le mode du handle » et « l'API appelée » ; à partir de .NET 6, File.OpenHandle et RandomAccess permettent une écriture directe où le mode et le décalage sont explicites.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.