Historique des révisions (première version, publiée le 29 Jul 2026)
- Première publication
Citer cet article(DOI (archive enregistrée): 10.5281/zenodo.22175270)
Les DOI ci-dessous renvoient à des versions déjà archivées et peuvent différer du texte actuel. Pour citer le texte actuel, utilisez l’URL de cette page.
Go Komura (2026). Les profondeurs de l'E/S Windows (partie 2) — E/S synchrone et asynchrone : ce que signifie vraiment OVERLAPPED. KomuraSoft LLC. https://comcomponent.com/fr/blog/windows-io-sync-async-overlapped/
- DOI (archive enregistrée)
- 10.5281/zenodo.22175270
- DOI (dernière version enregistrée)
- 10.5281/zenodo.22175271
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 d’une requête sont, dans le noyau, séparés. Cette fois, nous traitons l’E/S asynchrone (Overlapped I/O), c’est-à-dire la façon dont une application exploite cette séparation.
On a ajouté FILE_FLAG_OVERLAPPED et l’appel attend malgré tout. On a réutilisé une OVERLAPPED et les données se sont corrompues. On a libéré le tampon juste après l’annulation et le processus a planté. La clé pour comprendre ces cas est une seule répartition des rôles : le mode appartient au handle, l’état appartient à chaque opération, et le nettoyage n’intervient qu’après confirmation de l’achèvement.
Cet article suit l’ordre : ouvrir le fichier, émettre l’E/S, recevoir le résultat, nettoyer. Une fois le mécanisme Win32 en place, nous vérifions où se raccordent FileStream, ReadAsync et CancellationToken de .NET.
C’est la partie 2 de la série « Les profondeurs de l’E/S Windows ». Le plan d’ensemble se trouve au début de la partie 1.
1. D’abord la conclusion : trois distinctions faciles à confondre
L’E/S asynchrone devient difficile à raisonner si l’on s’en tient aux noms d’API. Commencez par séparer ce que l’on configure et le moment où l’on juge l’achèvement.
| Facile à confondre | Point de distinction |
|---|---|
| Le mode du handle et l’état de l’opération | Le mode synchrone ou asynchrone est fixé au moment de CreateFile. Une OVERLAPPED porte l’état d’une seule opération émise sur ce handle |
| Le résultat de l’émission et la réception de l’achèvement | ERROR_IO_PENDING n’est pas un échec, c’est une acceptation. TRUE signifie un achèvement synchrone, mais une notification arrive aussi par défaut. Ne traitez pas le résultat aux deux endroits |
| La demande d’annulation et le moment où l’on peut nettoyer | CancelIoEx est une demande d’annulation. On libère la structure et le tampon après avoir confirmé l’achèvement de cette opération |
L’E/S synchrone ne revient pas à l’appelant avant l’achèvement. L’E/S asynchrone offre un chemin qui revient avant. Toutefois, même en mode asynchrone, l’opération peut se terminer à l’intérieur de l’appel : ce n’est pas la garantie que « l’on n’attendra jamais ».12
Dans l’implémentation, raisonnez dans cet ordre : décider le mode → préparer une structure et un tampon dédiés à l’opération → juger le résultat de l’émission → recevoir l’achèvement → nettoyer. Même lorsque vous avez demandé l’annulation, n’omettez pas l’étape où l’achèvement est reçu.34
Si votre objectif est déjà fixé, partez du guide ci-dessous.
| Ce que vous voulez savoir ou le blocage rencontré | Où lire d’abord |
|---|---|
| En quoi l’E/S synchrone et l’E/S asynchrone diffèrent | Chapitre 2 : le mécanisme d’attente, section 3.1 : le mode du handle |
| Les données se corrompent avec OVERLAPPED, ou le processus plante après la sortie de la fonction | Section 3.2 : état et durée de vie par opération |
| ReadFile revient avec FALSE, ou un achèvement synchrone conduit à un double traitement | Section 3.3 : les trois branches du résultat d’émission |
| Vous voulez choisir comment recevoir l’achèvement, ou le callback n’arrive pas | Chapitre 4 : comparaison des méthodes de notification, section 4.3 : comment attendre un APC |
| Vous l’avez rendue asynchrone et l’appel attend malgré tout | Chapitre 5 : conditions d’achèvement synchrone et réactivité |
| L’annulation n’a pas d’effet, ou le processus plante après l’annulation | Chapitre 6 : nettoyer seulement après confirmation de l’achèvement |
| Les threads s’accumulent alors que vous utilisez ReadAsync | Chapitre 7 : combinaison des handles et des API .NET |
Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (36 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle
2. E/S synchrone : le thread qui attend l’achèvement s’endort sans consommer de CPU
2.1. C’est le gestionnaire d’E/S qui attend
Un handle ouvert sans FILE_FLAG_OVERLAPPED est en mode synchrone. ReadFile ne revient pas tant que l’E/S n’est pas achevée.1
Lorsque le pilote met la requête en attente (pending) parce qu’il attend la réponse du matériel, le gestionnaire d’E/S attend l’achèvement avant de rendre la main à l’application. Le thread de l’application attend dans le noyau pendant toute cette période.
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 dont la requête est passée en pending. ReadFile ne revient qu’après avoir attendu l’achèvement.
Cela dit, l’E/S synchrone ne met pas forcément le thread en sommeil. Une requête qui peut se terminer sur place, par exemple un cache hit, renvoie le résultat sans attendre (le chemin « achèvement immédiat » de la figure 5 de la partie 1). Ce qui est garanti, c’est seulement que l’appel ne revient pas avant l’achèvement.
2.2. Ne pas consommer de CPU et pouvoir faire autre chose sont deux choses distinctes
Un thread en état d’attente est retiré de l’ensemble des threads exécutables du scheduler, donc il ne consomme pas de CPU. Pourquoi il vaut mieux s’en remettre à l’attente plutôt que de continuer à interroger soi-même est aussi expliqué dans « Pourquoi préférer l’attente sur événement à Sleep(1) sous Windows ».
En revanche, un thread en attente ne peut pas faire d’autre travail. S’il s’agit du thread UI, l’écran se fige ; si un serveur consacre un thread à chaque connexion, quelques centaines de connexions font quelques centaines de threads. La faiblesse de l’E/S synchrone n’est pas l’utilisation CPU, c’est le fait que ce thread est indisponible jusqu’à l’achèvement.
En mode synchrone, le noyau gère aussi le pointeur de fichier (la position courante). C’est pourquoi des ReadFile successifs lisent « à la suite ». La position appartient à l’objet fichier derrière le handle, donc les handles dupliqués avec DuplicateHandle partagent la même position (partie 1, section 3.3).
Il existe aussi CancelSynchronousIo, qui demande l’annulation d’une E/S synchrone en cours d’exécution sur un autre thread. Le chapitre 6 résume comment le distinguer des API destinées à l’E/S asynchrone.5
3. Préparer et émettre une E/S asynchrone : séparer le mode, l’état et la valeur de retour
3.1. Le mode asynchrone se décide à l’ouverture du fichier
Passer FILE_FLAG_OVERLAPPED à CreateFile met l’objet fichier derrière le handle en mode asynchrone. Le mode n’est pas quelque chose que l’on bascule d’un appel à l’autre. On peut ouvrir le même fichier avec deux handles, l’un synchrone et l’autre asynchrone, et dans ce cas il y a aussi deux objets fichier.1
En mode asynchrone, le système ne gère pas de pointeur de fichier. Comme plusieurs opérations peuvent être émises en même temps, pour un fichier sur disque on indique à chaque fois la position de lecture ou d’écriture via OVERLAPPED.Offset / OffsetHigh. Pour un périphérique sans position de seek, comme un port série ou un tube nommé, cette position n’est pas utilisée et on la laisse à 0. Même lorsqu’on ne spécifie pas de position, une OVERLAPPED dédiée à l’opération reste nécessaire.6
À l’inverse, passer une OVERLAPPED à un handle en mode synchrone ne le rend pas asynchrone. Il lit depuis la position de Offset, mais le comportement de bloquer jusqu’à l’achèvement ne change pas. Ce qu’il faut vérifier, ce n’est pas si l’on a passé la structure, mais dans quel mode le handle a été ouvert.6
3.2. Faire correspondre une OVERLAPPED et un tampon à une opération
OVERLAPPED est la structure qui identifie une opération en cours d’émission et transporte sa position, son état et son résultat. La voir comme « le bordereau d’une opération » rend claire la répartition des rôles avec le handle.3
| Membre | Rôle |
|---|---|
Offset / OffsetHigh |
Position dans le fichier que cette opération lit ou écrit (indiquée à l’émission ; inutilisée sur un périphérique sans position) |
hEvent |
Événement signalé à l’achèvement (facultatif ; réinitialisation manuelle recommandée) |
Internal |
État de l’opération. Avant l’achèvement, il contient l’équivalent de STATUS_PENDING (usage système) |
InternalHigh |
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/>émission et achèvement sont séparés"]
end
subgraph O["Structure OVERLAPPED = bordereau d'une opération"]
F1["Offset : où lire"]
F2["hEvent : comment connaître l'achèvement"]
F3["Internal/InternalHigh :<br/>état et résultat (écrits par le système)"]
end
C["Fixé une seule fois, à CreateFile"] --> H
R["Une par émission de ReadFile/WriteFile"] --> O
Figure 2 : Le handle porte le mode ; OVERLAPPED porte la position et l’état de chaque opération.
Deux règles à tenir ici : le nombre et la durée de vie. Si vous émettez trois E/S en même temps, préparez aussi trois OVERLAPPED. Partager la même structure entre plusieurs opérations non achevées conduit à des résultats imprévisibles ou à une corruption de données.2
De plus, jusqu’à l’achèvement, gardez la structure et le tampon de données valides : ne les modifiez pas, ne les réutilisez pas, ne les libérez pas. Le noyau s’en sert encore. Si vous émettez avec une OVERLAPPED locale et quittez la fonction alors que l’opération n’est pas achevée, vous faites utiliser une zone de pile dont la durée de vie est déjà terminée.32
Lorsque vous réutilisez après confirmation de l’achèvement, réinitialisez pour qu’il ne reste rien de l’opération précédente. Si vous choisissez la méthode par événement, utilisez un événement à réinitialisation manuelle pour hEvent. Le lien avec la façon d’attendre est expliqué en section 4.2.3
3.3. Distinguer trois valeurs de retour de ReadFile et décider où les traiter
Le résultat d’une émission de ReadFile sur un handle asynchrone se juge par la combinaison de la valeur de retour et de GetLastError(). L’essentiel est de ne pas traiter tous les FALSE comme des échecs.6
Valeur de retour de ReadFile |
GetLastError() |
Signification | Ce que fait l’appelant |
|---|---|---|---|
TRUE |
(non consulté) | Achevé sur place (achèvement synchrone) | Par défaut, une notification d’achèvement arrive aussi séparément. Laisser le traitement du résultat au côté notification |
FALSE |
ERROR_IO_PENDING (997) |
Accepté. En cours | Ne rien faire. Attendre la notification d’achèvement sans toucher à OVERLAPPED ni au tampon |
FALSE |
Autre chose | L’émission elle-même a échoué | Aucune notification d’achèvement n’arrivera. Traiter l’erreur sur place et nettoyer OVERLAPPED et le tampon |
flowchart TB
A["ReadFile(handle asynchrone, avec OVERLAPPED)"]
Q{"Quelle est la valeur de retour ?"}
T["TRUE<br/>achevé sur place (achèvement synchrone)<br/>par défaut, une notification d'achèvement arrive aussi"]
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 4 méthodes du chapitre 4)"]
A --> Q
Q --> T
Q --> P
Q --> E
P --> W
Figure 3 : Traiter les trois branches : achèvement synchrone, accepté et en cours, échec d’émission.
La fonction ci-dessous ne fait que ce jugement et le renvoie à l’appelant. La préparation du handle, de la structure, du tampon et de l’événement dédiés à l’opération, ainsi que le traitement qui reçoit l’achèvement, sont supposés exister ailleurs.
// C++ / Win32
// hFile : handle ouvert avec FILE_FLAG_OVERLAPPED
// ov : OVERLAPPED allouée pour cette opération seule (Offset et hEvent déjà renseignés)
// buf/len: tampon dédié à cette opération. Ne pas le libérer avant la notification d'achèvement
DWORD IssueRead(HANDLE hFile, OVERLAPPED* ov, BYTE* buf, DWORD len)
{
// Pour une émission asynchrone, passer NULL à lpNumberOfBytesRead
// et récupérer le nombre d'octets transférés avec GetOverlappedResult après l'achèvement
if (ReadFile(hFile, buf, len, nullptr, ov))
{
// (1) Achèvement synchrone. Une notification arrive aussi par défaut, donc ne pas traiter le résultat ici
return ERROR_SUCCESS;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// (2) Accepté. Attendre la notification d'achèvement sans toucher à ov ni à buf
return ERROR_IO_PENDING;
}
// (3) Échec de l'émission elle-même. Aucune notification n'arrivera, donc l'appelant nettoie ici
return err;
}
ERROR_IO_PENDING est le résultat « accepté, pas encore achevé ». Il ne faut pas le nettoyer comme une erreur ordinaire. L’achèvement synchrone qui revient avec TRUE est tout autant un chemin normal, donc il faut toujours le traiter. Les raisons d’un achèvement synchrone sont expliquées au chapitre 5.
Traitez le résultat une seule fois. Par défaut, même pour une opération achevée de façon synchrone, un paquet d’achèvement est mis en file si le handle est associé à un IOCP, et l’événement est signalé si vous utilisez la méthode par événement. Traiter à la fois juste après TRUE et à la réception de la notification, c’est traiter deux fois la même opération et risquer de libérer deux fois la structure. La forme de base sûre consiste à ramener les deux chemins TRUE et ERROR_IO_PENDING vers le traitement du résultat côté notification.1
À l’inverse, le chemin où l’émission elle-même a échoué est celui où l’émetteur fait le traitement d’erreur et le nettoyage. Envoyer vers l’attente alors qu’aucune notification n’arrivera, c’est attendre indéfiniment.
Il existe aussi une optimisation qui omet la notification IOCP en cas d’achèvement synchrone, mais c’est un autre design que le comportement par défaut. Le champ d’application de FILE_SKIP_COMPLETION_PORT_ON_SUCCESS est traité à part en section 5.3.7
4. Choisir la notification d’achèvement : selon le nombre d’E/S et le thread qui les traite
Une fois l’émission et l’achèvement séparés, il faut un moyen de « recevoir l’achèvement ». Comparez d’abord les quatre méthodes selon le nombre d’E/S traitées en même temps et le thread qui exécute le traitement d’achèvement.1
| Méthode | Thread où s’exécute le traitement d’achèvement | Nombre d’E/S que l’on peut lancer en même temps | Cas d’usage |
|---|---|---|---|
| (1) Signalisation du handle | N’importe quel thread qui a attendu | Effectivement une. Avec plusieurs en vol, on ne distingue pas laquelle s’est achevée | Pratiquement aucun (4.1) |
(2) Événement + GetOverlappedResult |
N’importe quel thread qui a attendu | Un événement par opération. En les attendant ensemble avec WaitForMultipleObjects, le plafond 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 | Pas de 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 garder dans un seul thread (4.3) |
| (4) I/O completion port | Les threads de travail liés au port | Beaucoup d’E/S peuvent être reçues par peu de threads | Serveurs, pool de threads (4.4) |
flowchart TB
DONE["L'E/S s'achève dans le noyau<br/>(IoCompleteRequest, puis un APC fige le résultat)"]
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 mise dans la file APC du thread émetteur<br/>réception : elle s'exécute pendant une attente alertable telle que SleepEx"]
N4["(4) Un paquet d'achèvement entre dans l'I/O completion port<br/>réception : GetQueuedCompletionStatus (partie 3)"]
DONE --> N1
DONE --> N2
DONE --> N3
DONE --> N4
Figure 4 : Les quatre chemins de notification d’achèvement. La façon de recevoir dépend de la façon d’émettre.
4.1. Signalisation du handle : on ne distingue pas les opérations
Si vous émettez sans spécifier hEvent, c’est le handle de fichier lui-même qui passe à l’état signalé à l’achèvement. Mais lorsque plusieurs opérations sont en cours sur le même handle, on ne peut pas dire laquelle s’est achevée.1
Sauf le cas particulier où l’on n’émet jamais plus d’une E/S asynchrone à la fois, il est plus sûr de ne pas l’utiliser. Pratique en apparence, cela ne donne pas un mécanisme pour gérer le résultat par opération.
4.2. Événement et GetOverlappedResult : la forme de base pour quelques E/S simultanées
On place un événement à réinitialisation manuelle dans le OVERLAPPED.hEvent de chaque opération, puis on émet. Après avoir attendu avec WaitForSingleObject, on récupère le succès ou l’échec et le nombre d’octets transférés avec GetOverlappedResult. Pour attendre plusieurs événements ensemble, on utilise WaitForMultipleObjects, mais on ne peut en attendre que 64 à la fois.18
Mettre bWait de GetOverlappedResult à TRUE permet aussi d’attendre l’achèvement puis de prendre le résultat. Si vous utilisez ici un événement à réinitialisation automatique, GetOverlappedResult peut continuer d’attendre après qu’une autre attente a consommé le signal. On utilise la réinitialisation manuelle pour éviter ce problème de rendez-vous.83
Pour traiter solidement quelques E/S simultanées, c’est une méthode lisible. Elle sert aussi au « lire tout en écrivant » d’un port série. Pour un exemple concret, voir « Pièges des applications de communication série ».
4.3. APC : garder le thread émetteur en attente alertable jusqu’à l’achèvement
ReadFileEx / WriteFileEx sont la méthode où l’on spécifie une routine d’achèvement (un callback). Quand l’E/S s’achève, la routine est mise dans la file APC du thread qui l’a émise. Elle s’exécute lorsque ce thread entre en attente alertable via SleepEx, WaitForSingleObjectEx ou un appel similaire.91011
Comme le traitement d’achèvement s’exécute en série sur le même thread, un traitement qui reste dans un seul thread peut éviter les verrous. En revanche, si le thread émetteur n’entre jamais en attente alertable, la routine d’achèvement ne s’exécute pas. L’associer à une boucle de messages UI 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.
Avec un APC, les trois points faciles à manquer sont si l’émission a réussi, si la façon d’attendre est correcte, et si c’est bien votre opération qui s’est achevée. Le code ci-dessous est un extrait qui oppose une mauvaise attente et une bonne ; ce n’est pas un exemple où l’on exécute les deux à la suite. La préparation du handle et du tampon, ainsi que OnReadCompleted qui met à jour le drapeau d’achèvement de chaque opération, sont supposés exister ailleurs.
// C++ / Win32. hFile est un handle ouvert avec FILE_FLAG_OVERLAPPED,
// ov et buf sont supposés rester 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 pas livré
// Bon exemple : continuer une attente alertable jusqu'à la fin de cette E/S
//
// La routine d'achèvement lève ce drapeau (le porter par exemple dans la structure qui héberge ov)
volatile bool completed = false;
// Toujours vérifier si l'émission a réussi. Un retour 0 signifie qu'aucune routine d'achèvement n'a été mise en file
if (!ReadFileEx(hFile, buf, len, ov, OnReadCompleted))
{
const DWORD err = GetLastError(); // le prendre tout de suite. Les API suivantes l'écrasent
ReportError(err); // retrait du périphérique, handle invalide, etc.
return; // ★ il ne faut 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 s'est exécuté. Ce n'est pas forcément votre E/S,
// donc juger d'après completed, et attendre à nouveau sinon
continue;
}
// Revenu sur timeout. L'E/S est encore en vol, donc
// pour abandonner, annuler avec CancelIoEx et attendre que l'achèvement soit livré
CancelIoEx(hFile, ov);
}
Si l’émission échoue, n’entrez pas dans l’attente. Lorsque ReadFileEx renvoie 0, par exemple parce que le périphérique a été retiré ou que le handle est invalide, aucune routine d’achèvement n’a été mise en file. Prenez GetLastError() tout de suite, traitez l’erreur et sortez. Si vous manquez ce point, completed ne se lève jamais et vous répétez SleepEx et CancelIoEx contre une E/S qui n’existe pas.9
Ne prenez pas le timeout de l’attente pour la fin de l’E/S. Quand SleepEx expire, il quitte l’attente alertable, mais l’E/S déjà émise peut encore être en vol. Ne quittez pas la portée en laissant ov ou buf expirer à ce moment. Soit vous continuez d’attendre l’achèvement, soit vous abandonnez : demandez l’annulation et attendez que cet achèvement soit livré. La règle de durée de vie de la section 3.2 reste la même après un timeout.
Ne concluez pas de WAIT_IO_COMPLETION seul que votre E/S est terminée. Cette valeur de retour signifie qu’un ou plusieurs APC se sont exécutés. S’il y a une autre E/S ou un APC de QueueUserAPC dans le même thread, il revient aussi pour ceux-là. Jugez d’après le drapeau que met à jour votre propre routine d’achèvement, et attendez à nouveau s’il n’est pas encore levé.10
Quand « l’APC n’arrive pas », vérifiez la fonction d’attente en plus du succès de l’émission. Est-ce SleepEx(..., TRUE) et non Sleep, WaitForSingleObjectEx(..., TRUE) et non WaitForSingleObject ? Le Ex final et le TRUE de l’argument alertable sont les deux points à contrôler.10
4.4. IOCP : recevoir beaucoup d’E/S avec peu de workers
Avec un I/O completion port (IOCP), on associe le handle à un port. Les paquets d’achèvement entrent dans la file du port et les threads de travail les extraient avec GetQueuedCompletionStatus. C’est le mécanisme qui traite un grand nombre d’E/S simultanées avec peu de threads.12
C’est aussi le chemin qui sous-tend l’E/S asynchrone de .NET. Comment combiner la file des notifications d’achèvement et le contrôle du nombre de threads qui s’exécutent en parallèle est traité en détail la prochaine fois, dans la partie 3.
5. L’exception de l’achèvement synchrone : « asynchrone » n’est pas « on n’attend jamais »
5.1. Les conditions typiques d’un achèvement à l’intérieur de l’appel
Même émise correctement en mode asynchrone, l’E/S peut se terminer à l’intérieur de l’appel. L’achèvement synchrone signifie que l’E/S s’est terminée avant le retour de la fonction ; ce n’est pas la garantie d’un retour rapide. Distinguez le cas qui se termine vite grâce à un cache hit et le cas où l’on est mis en attente à l’intérieur de l’appel.2
flowchart TB
A["Émettre ReadFile/WriteFile sur un handle asynchrone"]
Q{"Une condition d'achèvement synchrone s'applique-t-elle ?"}
C1["Requête immédiatement satisfaisable<br/>(données déjà dans le cache, etc.)"]
C2["Fichier compressé NTFS<br/>(un fichier compressé ne devient pas asynchrone)"]
C3["Fichier chiffré NTFS (EFS)"]
C4["Écriture qui allonge la taille du fichier"]
T["Revient tout de suite avec TRUE<br/>= exécuté jusqu'à l'achèvement à l'intérieur de l'appel"]
P["Revient avec ERROR_IO_PENDING<br/>= vraiment 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"| P
Figure 5 : Principales conditions dans lesquelles une E/S émise de façon asynchrone s’achève de façon synchrone. À distinguer du temps que met l’appel à revenir.
Le document de dépannage Microsoft cite les raisons suivantes.2
| Condition | Pourquoi le traitement est synchrone, et ce que cela implique pour le code |
|---|---|
| Requête immédiatement satisfaisable, cache hit | Si les données sont en mémoire, le pilote peut achever sur place. Finir vite n’est pas un problème, mais le code qui suppose que ERROR_IO_PENDING revient toujours est cassé |
| Lecture avec cache activé, page nécessaire absente | Le cache de Windows est implémenté par file mapping. Il n’existe pas de mécanisme de défaut de page asynchrone, donc le traitement peut être synchrone |
| Fichier compressé NTFS ou chiffré EFS | Le pilote de système de fichiers convertit l’accès en synchrone |
| Écriture qui allonge le fichier | Une écriture qui change la longueur devient synchrone |
Le point important est qu’un traitement synchrone peut aussi se produire quand les données ne sont pas dans le cache, pas seulement en cas de cache hit. Le mécanisme du cache lui-même est traité dans la partie 4 de la série.
5.2. Concevoir à part les branches du résultat d’émission et la réactivité de l’UI
La première exigence est de traiter les trois branches de la section 3.3. Prévoir aussi TRUE comme un résultat normal et, par défaut, ramener le traitement du résultat vers le côté notification.
Cela dit, écrire correctement les branches ne garantit pas la réactivité. Comme on ne peut pas dire « l’UI ne se figera pas parce que l’E/S est asynchrone », il faut un design qui sépare l’émission elle-même de tout thread que l’on ne peut pas arrêter, et la confie à un thread dédié ou à un pool de threads. La pratique associée est aussi expliquée dans « Guide pratique pour se rapprocher autant que possible du temps réel souple sur un Windows ordinaire ».
5.3. Omettre la notification en cas d’achèvement synchrone est une optimisation propre à l’IOCP
Pour une E/S à haute fréquence, on peut optimiser en omettant la notification lors d’un achèvement synchrone. Activer FILE_SKIP_COMPLETION_PORT_ON_SUCCESS via SetFileCompletionNotificationModes fait que, pour une E/S réussie immédiatement, aucun paquet d’achèvement n’est mis dans l’IOCP. C’est le réglage à utiliser lorsque l’on bascule vers un design qui traite le résultat sur place, et non côté notification.7
Ce qui est omis, c’est seulement le paquet vers l’IOCP : la signalisation de OVERLAPPED.hEvent n’est pas empêchée. N’appliquez pas la même optimisation à la méthode par événement. Mélanger le chemin de notification par défaut et le chemin optimisé conduit au double traitement de la section 3.3 ou à une attente de notification erronée. La combinaison avec l’IOCP est traitée dans la partie 3.
6. Annulation et fin de traitement : demander, confirmer l’achèvement, fermer
6.1. Choisir l’API selon ce que l’on annule
Les API d’annulation se distinguent selon l’opération et le thread émetteur.4135
| API | Cible et façon de la spécifier |
|---|---|
CancelIoEx |
Demande l’annulation des E/S non achevées d’un handle donné, quel que soit le thread qui les a émises. Un OVERLAPPED en deuxième argument cible cette opération ; NULL cible toutes les opérations de ce handle |
CancelIo |
Cible uniquement les opérations émises par le thread appelant lui-même |
CancelSynchronousIo |
Cible une E/S synchrone en cours d’exécution sur un autre thread spécifié |
CancelIoEx a été introduit avec Vista. Pour l’E/S asynchrone, il n’y a plus de raison aujourd’hui d’utiliser délibérément l’ancien CancelIo limité au thread émetteur : prenez CancelIoEx comme base.
6.2. Le succès de CancelIoEx ne signifie pas la fin de l’E/S
CancelIoEx est une API qui demande l’annulation des IRP non achevés, pas une API qui attend la fin de l’opération. Un succès signifie seulement que l’annulation a été demandée. Une opération déjà proche de l’achèvement peut se terminer normalement parce que l’annulation n’est pas arrivée à temps.14
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 de l'IRP non achevé<br/>correspondant (le marque)
IOM->>DRV: Appel de la routine d'annulation
Note over DRV: Interrompt si l'état le permet<br/>si l'achèvement est imminent, peut se terminer normalement
DRV->>IOM: IoCompleteRequest<br/>(STATUS_CANCELLED)
IOM-->>App: La notification d'achèvement arrive<br/>GetOverlappedResult renvoie ERROR_OPERATION_ABORTED
Note over App: Libérer OVERLAPPED et le tampon<br/>seulement après avoir vu cette notification
Figure 6 : Une opération annulée est aussi notifiée comme un achèvement. Le nettoyage se fait après cette confirmation.
Une opération réellement annulée revient dans la notification d’achèvement comme ERROR_OPERATION_ABORTED. Qu’elle se soit terminée normalement ou qu’elle ait été annulée, ne libérez ni la structure ni le tampon avant d’avoir reçu la notification. Les libérer trop tôt, c’est faire perdre au noyau une zone qu’il utilise encore, ce qui mène à une corruption mémoire. En cas de violation d’accès après une annulation, vérifiez d’abord cette durée de vie.414
6.3. Récupérer les opérations déjà émises avant de fermer le handle
La séquence de base en fin de traitement est demander l’annulation → voir l’achèvement jusqu’au bout → fermer le handle.
Comme nous l’avons vu dans la partie 1, fermer le dernier handle déclenche un traitement de cleanup qui annule les IRP non achevés. Mais fermer le handle en laissant des E/S émises en suspens tend à faire s’écrouler la gestion des notifications d’achèvement et de la durée de vie des tampons. Ne déléguez pas le nettoyage à la fermeture : terminez d’abord les opérations non achevées.
Il en va de même lorsque vous voulez abandonner sur timeout. L’OS ne décide pas à votre place les critères d’abandon de l’application, donc concevez ensemble l’annulation après timeout et la procédure pour recevoir l’achèvement. Avec la méthode APC, continuez l’attente alertable jusqu’à la livraison de l’achèvement, comme en section 4.3.
7. Correspondance avec .NET : regarder aussi l’endroit où le fichier est ouvert, pas seulement ReadAsync
7.1. Accorder le mode du handle et l’API appelée
Le useAsync de FileStream, ou FileOptions.Asynchronous, correspond au FILE_FLAG_OVERLAPPED de Win32. Comme dans le tableau de correspondance de la partie 1, en .NET aussi le mode au moment d’ouvrir le fichier est essentiel.1516
flowchart TB
A["await fs.ReadAsync(...)"]
Q{"Le handle est-il en mode asynchrone<br/>(FileOptions.Asynchronous) ?"}
Y["Véritable E/S asynchrone<br/>l'équivalent d'OVERLAPPED est émis et<br/>l'achèvement arrive au pool de threads via l'IOCP (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 le même ReadAsync, le mode du handle change le chemin de traitement côté OS.
| Combinaison handle et API | Ce qui se passe en interne |
|---|---|
Mode asynchrone + ReadAsync / WriteAsync |
La combinaison qui utilise l’E/S asynchrone de l’OS |
Mode synchrone + ReadAsync / WriteAsync |
Un thread du pool de threads prend en charge la lecture/écriture synchrone : « asynchrone de façade » |
Mode asynchrone + Read / Write synchrones |
Un surcoût d’attente de l’achèvement apparaît en interne |
Même avec l’« asynchrone de façade », le thread appelant n’est pas mis en attente, mais un autre thread attend 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 et de baisse de scalabilité. Accorder le mode et l’API est le principe.1615
7.2. Comparer trois façons de créer un FileStream
Dans (A) et (B) ci-dessous, ReadAsync est appelé de la même façon. La seule différence est useAsync à l’ouverture du fichier. (C) est l’exemple, à partir de .NET 6, qui rend le handle et la position explicites.
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 raccorde directement à FILE_FLAG_OVERLAPPED
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true))
{
// L'achèvement arrive au pool de threads via l'IOCP (partie 3)
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (C) .NET 6 et ultérieur. Écriture directe qui rend le mode et le décalage explicites
using (SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read,
options: FileOptions.Asynchronous))
{
int read = await RandomAccess.ReadAsync(handle, buffer, fileOffset: 0);
}
Pour auditer du code existant, ne cherchez pas seulement les appels à ReadAsync / WriteAsync, mais les endroits où FileStream est créé. File.OpenRead et les surcharges courtes comme new FileStream(path, FileMode.Open) ouvrent en mode synchrone. Lorsque vous construisez un FileStream à partir d’un SafeFileHandle, alignez aussi l’argument isAsync sur le mode réel du handle.
7.3. Avec RandomAccess, le handle et le décalage sont explicites
Dans .NET 6, l’implémentation interne de FileStream a été entièrement réécrite, et File.OpenHandle et RandomAccess ont été ajoutés. Ce sont des API où l’on traite un SafeFileHandle directement et où l’on passe à chaque fois la position de lecture ou d’écriture.16
La forme de (C), qui rend le mode et fileOffset explicites, correspond à la répartition des rôles décrite dans cet article : un handle asynchrone et un OVERLAPPED.Offset par opération.
7.4. Même avec CancellationToken, l’annulation reste une demande
Pour un handle en mode asynchrone, l’annulation d’une E/S de fichier via CancellationToken aboutit en interne à CancelIoEx. Derrière un ReadAsync auquel on a passé un jeton et qui se termine par une OperationCanceledException, c’est le mécanisme du chapitre 6 qui fonctionne. Le point selon lequel une interruption immédiate n’est pas garantie est le même.
Dans l’« asynchrone de façade » en mode synchrone, il n’existe pas d’opération overlapped à annuler, donc ce chemin est inutilisable. Les runtimes .NET récents incluent aussi un mécanisme qui tente d’annuler, via CancelSynchronousIo, un appel en cours d’exécution synchrone, mais le comportement dépend de la version du runtime et du type d’opération, et une interruption certaine n’est pas garantie. Si vous concevez autour de l’annulation, la voie principale est d’aligner le mode du handle et d’utiliser l’E/S asynchrone de l’OS.
Pour la pratique au-dessus de async/await — ConfigureAwait, le rapport avec le thread UI, etc. — voir « 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 explique comment l’OS fait avancer les lectures et écritures en dessous.
8. Synthèse : vérifier d’un seul tenant, de l’émission au nettoyage
Pour inspecter une E/S asynchrone, suivez le code dans cet ordre.
- À l’ouverture, confirmez le mode synchrone ou asynchrone. Pour un fichier sur disque, indiquez la position à chaque opération asynchrone.
- À l’émission, confirmez qu’il y a une
OVERLAPPEDet un tampon dédiés à l’opération, et que les trois branches de la section 3.3 sont traitées. - À la réception de l’achèvement, confirmez que la façon d’attendre correspond à la méthode — événement, APC, IOCP — et que le même résultat n’est pas traité deux fois.
- À la fin, confirmez que vous ne libérez pas sur la seule foi d’un timeout ou d’une demande d’annulation.
L’E/S synchrone et l’E/S asynchrone ne sont pas deux tuyauteries distinctes. La différence est de revenir après avoir attendu l’achèvement, ou d’emprunter un chemin qui revient avant. Toutefois, un achèvement synchrone se produit aussi en mode asynchrone, donc séparez les branches du résultat d’émission et la conception de la réactivité.12
Le mode appartient au handle, l’état appartient à chaque opération, et le nettoyage n’intervient qu’après confirmation de l’achèvement. Cette répartition des rôles est la même que l’on traite directement l’OVERLAPPED Win32 ou que l’on utilise FileOptions.Asynchronous de .NET. Une opération annulée, elle aussi, reste sous gestion jusqu’à réception de l’achèvement.3415
La suite est la partie 3 : « Les profondeurs de l’E/S Windows (partie 3) — Les ports d’achèvement d’E/S (IOCP) et le pool de threads .NET : le sous-sol d’async/await ». Elle traite pourquoi l’IOCP de la section 4.4 unifie la file des notifications d’achèvement et le contrôle du nombre de threads d’exécution, et sur quel thread se poursuit un await.
Articles associés
- Les profondeurs de l’E/S 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
KomuraSoft LLC traite la conception d’applications métier Windows et d’applications de communication avec des périphériques qui utilisent l’E/S asynchrone, ainsi que l’investigation des causes de dysfonctionnements du type « ça se fige », « ça plante à l’annulation » ou « le pool de threads s’épuise ».
- Développement d’applications Windows
- Analyse de dysfonctionnements et des causes racines
- Développement d’applications Windows à temps réel souple
- Nous contacter
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, tandis qu’en E/S asynchrone la fonction qui émet la requête revient tout de suite et le thread peut continuer d’autre travail ; 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 spécifié dans la structure OVERLAPPED, routine d’achèvement (APC) exécutée pendant une attente alertable, I/O completion port ; et sur le fait que, lorsque plusieurs opérations sont émises en même temps, la signalisation du handle de fichier ne permet pas de distinguer laquelle s’est achevée. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Sur les raisons pour lesquelles une E/S écrite pour l’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 de façon asynchrone à un fichier compressé, donc toutes les opérations deviennent synchrones), un fichier chiffré NTFS, une écriture qui allonge la longueur du fichier, et le cas où le pilote achève l’opération sur place et renvoie TRUE parce que la requête peut être satisfaite immédiatement (données déjà dans le cache en mémoire, etc.) ; sur le fait que le cache de Windows est implémenté par file mapping et qu’il n’existe pas de mécanisme de défaut de page asynchrone lorsque la page est absente ; et, en plus, sur le fait que trois E/S exigent trois structures OVERLAPPED, que les réutiliser conduit à des résultats imprévisibles ou à une corruption de données, et qu’il ne faut ni lire ni écrire le tampon de données correspondant tant que l’opération n’est pas achevée. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, OVERLAPPED structure. Sur le fait que la structure OVERLAPPED conserve les informations pour une entrée/sortie asynchrone ; sur Offset/OffsetHigh qui portent la position dans le fichier, hEvent l’événement signalé à l’achèvement, Internal/InternalHigh le code d’état de l’opération et le nombre d’octets transférés ; sur l’interdiction de modifier la structure pendant l’opération et l’obligation de la garder valide ; et sur les précautions lorsque l’on utilise un événement. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
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 que spécifier lpOverlapped cible cette opération seule, et NULL 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, d’où la nécessité d’attendre que le traitement d’achèvement soit terminé. ↩ ↩2 ↩3 ↩4
-
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 le thread spécifié, et sur le fait que l’opération annulée revient en échec avec ERROR_OPERATION_ABORTED. ↩ ↩2
-
Microsoft Learn, ReadFile function. Sur le fait que, pour un handle ouvert avec FILE_FLAG_OVERLAPPED, lpOverlapped est obligatoire et que la position de début de lecture se spécifie via Offset/OffsetHigh de la structure OVERLAPPED ; sur le retour FALSE et ERROR_IO_PENDING lorsque le traitement est asynchrone ; sur le fait que le système ne maintient pas de pointeur de fichier pour un handle asynchrone ; et sur le fait que, si l’on passe une OVERLAPPED à un handle ouvert sans FILE_FLAG_OVERLAPPED, la lecture se fait depuis le décalage indiqué mais ReadFile ne revient pas avant la fin de la lecture. ↩ ↩2 ↩3
-
Microsoft Learn, SetFileCompletionNotificationModes function. Sur le fait que FILE_SKIP_COMPLETION_PORT_ON_SUCCESS permet de ne pas mettre de paquet d’achèvement dans l’I/O completion port lorsque l’E/S réussit immédiatement, et sur le fait que FILE_SKIP_SET_EVENT_ON_HANDLE permet d’omettre la mise 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/échec et nombre d’octets transférés) ; sur le fait que passer TRUE à bWait fait attendre jusqu’à l’achèvement ; et sur le risque que, si hEvent de OVERLAPPED est un événement à réinitialisation automatique et qu’une autre attente a consommé le signal, un appel avec bWait=TRUE ne détecte pas l’achèvement et continue d’attendre, d’où l’usage d’un événement à réinitialisation manuelle. ↩ ↩2
-
Microsoft Learn, ReadFileEx function. Sur le fait que ReadFileEx reçoit une routine d’achèvement (FileIOCompletionRoutine) appelée à la fin de la lecture ; sur le fait que la routine 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. ↩ ↩2
-
Microsoft Learn, Alertable I/O. Sur le fait qu’en alertable I/O une entrée vers la routine d’achèvement est mise dans la file 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 qu’un APC s’exécute toujours dans le contexte du thread qui l’a émis. ↩ ↩2 ↩3
-
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 a sa propre file 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, I/O Completion Ports. Sur le fait qu’un I/O completion port 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 met les paquets d’achèvement en file et que les threads de travail les extraient avec GetQueuedCompletionStatus ; et sur le fait que le port contrôle le nombre de threads qui s’exécutent en parallèle. ↩
-
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 d’utiliser CancelIoEx pour annuler aussi celles émises par d’autres threads. ↩
-
Microsoft Learn, Canceling pending I/O operations. Sur le mécanisme d’annulation des E/S non achevées, sur le fait qu’une opération peut déjà être en train de s’achever même après une demande d’annulation, sur le fait de confirmer l’achèvement d’une opération annulée avant de libérer les ressources, et sur le partage : CancelSynchronousIo pour les opérations synchrones, CancelIo/CancelIoEx pour les opérations asynchrones. ↩ ↩2
-
Microsoft Learn, Asynchronous file I/O (.NET). Sur la conception de l’E/S de fichier asynchrone en .NET, sur le fait de spécifier useAsync (FileOptions.Asynchronous) dans le constructeur de FileStream pour activer l’E/S asynchrone au niveau OS, et sur le partage d’usage entre méthodes synchrones et asynchrones. ↩ ↩2 ↩3
-
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 diverge selon que le handle a été ouvert en mode asynchrone, sur le fait que File.OpenHandle obtient un SafeFileHandle directement et que RandomAccess permet des lectures/écritures (thread-safe) avec un décalage explicite, et sur le fait qu’un appel asynchrone sur un handle qui n’est pas en mode asynchrone est déchargé vers le pool de threads. ↩ ↩2 ↩3
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Les profondeurs de l'E/S Windows (partie 4) — Cache Manager : quand votre WriteFile atteint-il vraiment le disque ?
Quatrième partie de la série qui explique le Cache Manager de Windows à l'aide de schémas. Cache implémenté comme un mappage de fichiers,...
Les profondeurs de l'E/S 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 partie de la série qui explique le port d'achèvement d'E/S (IOCP) à l'aide de schémas. Conception qui unifie la file d'achèveme...
Les profondeurs de l'E/S 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'E/S Windows (partie 6, dernière partie) — Fonctionnement des minifilters et enquête de latence avec Procmon
Explique comment les minifilters surveillent et contrôlent les E/S fichiers : FltMgr, altitudes, rappels pre/post, fltmc, repérage des op...
Les profondeurs de l'E/S Windows (partie 5) — Structure interne de NTFS : comprendre le système de fichiers à travers la MFT
Cinquième partie de la série qui explique la structure interne de NTFS à l'aide de schémas. MFT et enregistrements de fichiers, flux de d...
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, I/O completion port, 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 termine 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 l'I/O completion port) 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.