Änderungsverlauf (Erstfassung, veröffentlicht am 29. Jul 2026)
- Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22175262)
Die folgenden DOIs verweisen auf bereits archivierte Versionen, die vom aktuellen Text abweichen können. Verwenden Sie die URL dieser Seite, um auf den aktuellen Text zu verweisen.
Go Komura (2026). Die Tiefen von Windows I/O (Teil 2) — Synchrones und asynchrones I/O: Was OVERLAPPED wirklich bedeutet. KomuraSoft LLC. https://comcomponent.com/de/blog/windows-io-sync-async-overlapped/
- DOI (registriertes Archiv)
- 10.5281/zenodo.22175262
- DOI (zuletzt registrierte Version)
- 10.5281/zenodo.22175263
Im letzten Teil (Teil 1) haben wir gesehen, dass eine Windows-I/O-Anfrage zu einem Paket namens IRP wird, das durch den Gerätestapel fließt, und dass Ausgabe und Abschluss einer Anfrage im Kernel getrennt sind. Diesmal geht es um asynchrones I/O (Overlapped I/O), mit dem eine Anwendung diese Trennung nutzt.
Sie setzen FILE_FLAG_OVERLAPPED, und der Aufruf wartet trotzdem. Sie verwenden OVERLAPPED mehrfach, und die Daten werden beschädigt. Sie geben den Puffer direkt nach dem Abbruch frei, und der Prozess stürzt ab. Der Schlüssel zu allen dreien ist eine Aufteilung: Der Modus gehört zum Handle, der Zustand zu jedem Vorgang, und aufgeräumt wird erst, nachdem der Abschluss bestätigt ist.
Dieser Artikel folgt der Reihenfolge: Datei öffnen, I/O ausgeben, Ergebnis entgegennehmen, aufräumen. Wenn der Win32-Mechanismus klar ist, prüfen wir, wo FileStream, ReadAsync und CancellationToken in .NET daran anschließen.
Das ist Teil 2 der Serie „Die Tiefen von Windows I/O“. Der Gesamtaufbau steht am Anfang von Teil 1.
1. Zuerst das Fazit: drei Unterscheidungen, die leicht vermischt werden
Asynchrones I/O wird unübersichtlich, wenn man es nur am API-Namen festmacht. Trennen Sie zuerst, was Sie konfigurieren, von dem Zeitpunkt, an dem der Abschluss feststeht.
| Leicht zu verwechseln | Woran man sie unterscheidet |
|---|---|
| Modus des Handles und Zustand des Vorgangs | Synchroner oder asynchroner Modus steht im Moment von CreateFile fest. OVERLAPPED hält den Zustand eines einzelnen Vorgangs, der an dieses Handle ausgegeben wird |
| Ergebnis der Ausgabe und Empfang des Abschlusses | ERROR_IO_PENDING ist kein Fehler, sondern Annahme. TRUE bedeutet synchronen Abschluss, standardmäßig trifft aber trotzdem eine Benachrichtigung ein. Verarbeiten Sie das Ergebnis nicht an beiden Stellen |
| Abbruchanforderung und der Zeitpunkt, an dem aufgeräumt werden darf | CancelIoEx ist die Bitte um Abbruch. Struktur und Puffer geben Sie erst frei, nachdem der Abschluss dieses Vorgangs bestätigt ist |
Synchrones I/O kehrt nicht zum Aufrufer zurück, bevor der Vorgang abgeschlossen ist. Asynchrones I/O gibt einen Weg, der vor dem Abschluss zurückkehrt. Allerdings kann ein Vorgang auch im asynchronen Modus innerhalb des Aufrufs abschließen; das ist keine Garantie, dass Sie niemals warten müssen.12
In der Implementierung denken Sie in dieser Reihenfolge: Modus festlegen → struktur- und pufferspezifisch für den Vorgang vorbereiten → das Ergebnis der Ausgabe beurteilen → den Abschluss entgegennehmen → aufräumen. Auch wenn Sie einen Abbruch angefordert haben, überspringen Sie die Stufe, in der der Abschluss empfangen wird, nicht.34
Wenn das Ziel schon feststeht, beginnen Sie bei der folgenden Übersicht.
| Was Sie wissen oder klären wollen | Zuerst lesen |
|---|---|
| Worin sich synchrones und asynchrones I/O unterscheiden | Kapitel 2: der Warte-Mechanismus, Abschnitt 3.1: der Modus des Handles |
| Mit OVERLAPPED werden Daten beschädigt, oder es stürzt nach dem Verlassen der Funktion ab | Abschnitt 3.2: Zustand und Lebensdauer pro Vorgang |
| ReadFile kehrt mit FALSE zurück, oder synchroner Abschluss führt zu doppelter Verarbeitung | Abschnitt 3.3: die dreifache Verzweigung des Ausgabeergebnisses |
| Sie wollen die Empfangsmethode wählen, oder der Callback kommt nicht | Kapitel 4: Vergleich der Benachrichtigungswege, Abschnitt 4.3: Warten auf eine APC |
| Sie haben asynchron gemacht, und der Aufruf wartet trotzdem | Kapitel 5: Bedingungen für synchronen Abschluss und Reaktionsfähigkeit |
| Der Abbruch wirkt nicht, oder es stürzt nach dem Abbruch ab | Kapitel 6: erst nach bestätigtem Abschluss aufräumen |
| Trotz ReadAsync wächst die Zahl der Threads | Kapitel 7: Kombination aus Handle und API in .NET |
In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (36 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle
2. Synchrones I/O: Ein Thread, der auf den Abschluss wartet, schläft ohne CPU-Verbrauch
2.1. Wer wartet, ist der I/O-Manager
Ein ohne FILE_FLAG_OVERLAPPED geöffnetes Handle befindet sich im synchronen Modus. ReadFile kehrt nicht zurück, bevor das I/O abgeschlossen ist.1
Stellt der Treiber die Anfrage zurück (pending), weil er auf die Antwort der Hardware wartet, wartet der I/O-Manager auf den Abschluss und gibt dann die Kontrolle an die Anwendung zurück. Der Anwendungs-Thread wartet in dieser Zeit im Kernel.
sequenceDiagram
participant App as Anwendungs-Thread
participant IOM as I/O-Manager
participant DRV as Treiber (Stapel)
App->>IOM: ReadFile(synchrones Handle)
IOM->>DRV: IRP ausgeben
DRV-->>IOM: STATUS_PENDING (wartet auf Antwort)
Note over App,IOM: Der Thread wechselt im Kernel in den Wartezustand<br/>und schläft, ohne CPU zu verbrauchen
DRV->>IOM: Abschluss (IoCompleteRequest)
IOM-->>App: Ergebnis zurückgeben und aufwecken<br/>ReadFile kehrt mit TRUE/FALSE zurück
Abbildung 1: Synchrones I/O, bei dem die Anfrage zurückgestellt wurde. ReadFile kehrt erst zurück, nachdem auf den Abschluss gewartet wurde
Allerdings schläft ein Thread bei synchronem I/O nicht immer. Eine Anfrage, die sich an Ort und Stelle abschließen lässt, etwa ein Cache-Treffer, gibt das Ergebnis ohne Warten zurück (der Weg „sofortiger Abschluss“ aus Abbildung 5 in Teil 1). Garantiert ist nur, dass der Aufruf nicht vor dem Abschluss zurückkehrt.
2.2. Keine CPU zu verbrauchen und andere Arbeit tun zu können sind verschiedene Dinge
Ein Thread im Wartezustand fällt aus der Menge der lauffähigen Threads des Schedulers, verbraucht also keine CPU. Warum Warten besser ist als selbst zu pollen, erklärt auch „Warum Sie unter Windows Ereigniswarten gegenüber Sleep(1) bevorzugen sollten“.
Ein wartender Thread kann andererseits keine andere Arbeit tun. Ist es der UI-Thread, friert die Oberfläche ein; richtet ein Server pro Verbindung einen Thread ein, wächst die Zahl der Threads schon bei ein paar hundert Verbindungen. Die Schwäche von synchronem I/O ist nicht die CPU-Auslastung, sondern dass dieser Thread bis zum Abschluss nicht zur Verfügung steht.
Im synchronen Modus führt der Kernel auch den Dateizeiger (die aktuelle Position). Deshalb lesen aufeinanderfolgende ReadFile-Aufrufe „an der Stelle weiter“. Die Position gehört zum Dateiobjekt hinter dem Handle, sodass mit DuplicateHandle duplizierte Handles sich die Position teilen (Teil 1, Abschnitt 3.3).
Es gibt außerdem CancelSynchronousIo, das den Abbruch eines auf einem anderen Thread laufenden synchronen I/O anfordert. Die Abgrenzung zu den APIs für asynchrones I/O fasst Kapitel 6 zusammen.5
3. Vorbereitung und Ausgabe von asynchronem I/O: Modus, Zustand und Rückgabewert trennen
3.1. Der asynchrone Modus wird beim Öffnen der Datei festgelegt
Übergeben Sie CreateFile FILE_FLAG_OVERLAPPED, wird das Dateiobjekt hinter dem Handle in den asynchronen Modus versetzt. Der Modus lässt sich nicht pro Aufruf umschalten. Dieselbe Datei können Sie mit zwei Handles öffnen, einem für synchrone und einem für asynchrone Nutzung; dann gibt es auch zwei Dateiobjekte.1
Im asynchronen Modus führt das System keinen Dateizeiger. Weil mehrere Vorgänge gleichzeitig unterwegs sein können, geben Sie bei einer Datei auf dem Datenträger die Lese- und Schreibposition jedes Mal über OVERLAPPED.Offset / OffsetHigh an. Bei Geräten ohne Seek-Position, etwa seriellen Ports oder benannten Pipes, wird diese Positionsangabe nicht verwendet; lassen Sie sie auf 0. Auch wenn Sie keine Position angeben, brauchen Sie ein vorgangseigenes OVERLAPPED.6
Umgekehrt macht die Übergabe eines OVERLAPPED an ein Handle im synchronen Modus es nicht asynchron. Es wird ab der Position in Offset gelesen, das Blockieren bis zum Abschluss ändert sich aber nicht. Entscheidend ist nicht, ob Sie die Struktur übergeben haben, sondern in welchem Modus das Handle geöffnet wurde.6
3.2. OVERLAPPED und Puffer einem Vorgang zuordnen
OVERLAPPED ist die Struktur, die einen laufenden Vorgang identifiziert und seine Position, seinen Zustand und sein Ergebnis trägt. Denken Sie an „den Beleg für einen Vorgang“, dann wird die Aufteilung gegenüber dem Handle klar.3
| Member | Aufgabe |
|---|---|
Offset / OffsetHigh |
Die Position in der Datei, die dieser Vorgang liest oder schreibt (bei der Ausgabe angegeben; bei Geräten ohne Position ungenutzt) |
hEvent |
Ein bei Abschluss signalisiertes Ereignis (optional; manuelles Zurücksetzen empfohlen) |
Internal |
Der Zustand des Vorgangs. Vor dem Abschluss steht etwas wie STATUS_PENDING darin (für das System) |
InternalHigh |
Die übertragene Bytezahl bei Abschluss (für das System) |
flowchart LR
subgraph H["Handle (Dateiobjekt) = Modus"]
M1["Synchroner Modus<br/>Kernel führt die aktuelle Position<br/>ReadFile kehrt erst bei Abschluss zurück"]
M2["Asynchroner Modus (FILE_FLAG_OVERLAPPED)<br/>Aktuelle Position wird nicht geführt<br/>Ausgabe und Abschluss sind getrennt"]
end
subgraph O["OVERLAPPED-Struktur = Beleg für einen Vorgang"]
F1["Offset: wo gelesen wird"]
F2["hEvent: wie der Abschluss erfahren wird"]
F3["Internal/InternalHigh:<br/>Zustand und Ergebnis (vom System geschrieben)"]
end
C["Wird einmalig bei CreateFile festgelegt"] --> H
R["Bei jeder Ausgabe von ReadFile/WriteFile eines vorbereiten"] --> O
Abbildung 2: Das Handle hält den Modus, OVERLAPPED die Position und den Zustand jedes Vorgangs
Zu beachten sind hier Anzahl und Lebensdauer. Geben Sie drei I/Os gleichzeitig aus, bereiten Sie drei OVERLAPPED vor. Teilen sich mehrere nicht abgeschlossene Vorgänge dieselbe Struktur, führt das zu unvorhersehbaren Ergebnissen oder Datenbeschädigung.2
Außerdem halten Sie Struktur und Datenpuffer bis zum Abschluss gültig: Inhalt nicht ändern, nicht wiederverwenden, nicht freigeben. Der Kernel verwendet diesen Bereich noch. Geben Sie mit einem lokalen OVERLAPPED aus und verlassen die Funktion, während der Vorgang noch aussteht, überlassen Sie dem Kernel einen Stackbereich, dessen Lebensdauer vorbei ist.32
Wenn Sie nach bestätigtem Abschluss wiederverwenden, initialisieren Sie neu, damit kein Zustand des vorherigen Vorgangs übrig bleibt. Wählen Sie den Ereignisweg, verwenden Sie für hEvent ein manuell zurückzusetzendes Ereignis. Den Zusammenhang mit der Art des Wartens erklärt Abschnitt 4.2.3
3.3. Den Rückgabewert von ReadFile dreifach teilen und den Verarbeitungsort festlegen
Das Ergebnis einer ReadFile-Ausgabe an ein asynchrones Handle beurteilen Sie anhand der Kombination aus Rückgabewert und GetLastError(). Wichtig ist, nicht jedes FALSE als Fehlschlag zu behandeln.6
Rückgabewert von ReadFile |
GetLastError() |
Bedeutung | Was der Aufrufer tut |
|---|---|---|---|
TRUE |
(nicht prüfen) | An Ort und Stelle abgeschlossen (synchroner Abschluss) | Standardmäßig trifft zusätzlich eine Abschlussbenachrichtigung ein. Die Ergebnisverarbeitung dem Benachrichtigungsweg überlassen |
FALSE |
ERROR_IO_PENDING (997) |
Angenommen. In Bearbeitung | Nichts tun. Auf die Abschlussbenachrichtigung warten, ohne OVERLAPPED oder Puffer anzufassen |
FALSE |
Alles andere | Die Ausgabe selbst ist fehlgeschlagen | Es trifft keine Abschlussbenachrichtigung ein. Sofort den Fehler behandeln und OVERLAPPED sowie Puffer aufräumen |
flowchart TB
A["ReadFile(asynchrones Handle, mit OVERLAPPED)"]
Q{"Wie lautet der Rückgabewert?"}
T["TRUE<br/>an Ort und Stelle abgeschlossen (synchroner Abschluss)<br/>standardmäßig trifft zusätzlich eine Abschlussbenachrichtigung ein"]
P["FALSE + ERROR_IO_PENDING<br/>Angenommen. Der Abschluss wird später gemeldet"]
E["FALSE + ein anderer Fehler<br/>Die Ausgabe selbst ist fehlgeschlagen"]
W["Auf die Abschlussbenachrichtigung warten<br/>(die vier Wege aus Kapitel 4)"]
A --> Q
Q --> T
Q --> P
Q --> E
P --> W
Abbildung 3: Die dreifache Verzweigung aus synchronem Abschluss, Annahme mit laufendem Vorgang und fehlgeschlagener Ausgabe
Die folgende Funktion führt nur diese Beurteilung aus und kehrt zum Aufrufer zurück. Vorbereitung von Handle, vorgangseigener Struktur, Puffer und Ereignis sowie die Entgegennahme des Abschlusses werden woanders vorausgesetzt.
// C++ / Win32
// hFile : ein mit FILE_FLAG_OVERLAPPED geöffnetes Handle
// ov : ein nur für diesen Vorgang reserviertes OVERLAPPED (Offset und hEvent sind bereits gesetzt)
// buf/len: ein nur für diesen Vorgang reservierter Puffer. Erst nach der Abschlussbenachrichtigung freigeben
DWORD IssueRead(HANDLE hFile, OVERLAPPED* ov, BYTE* buf, DWORD len)
{
// Bei asynchroner Ausgabe NULL für lpNumberOfBytesRead übergeben;
// die übertragene Bytezahl später mit GetOverlappedResult nach Abschluss holen
if (ReadFile(hFile, buf, len, nullptr, ov))
{
// (1) Synchroner Abschluss. Standardmäßig trifft trotzdem eine Abschlussbenachrichtigung ein,
// deshalb hier das Ergebnis nicht verarbeiten
return ERROR_SUCCESS;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// (2) Angenommen. Auf die Abschlussbenachrichtigung warten, ohne ov oder buf anzufassen
return ERROR_IO_PENDING;
}
// (3) Die Ausgabe selbst ist fehlgeschlagen. Es trifft keine Abschlussbenachrichtigung ein,
// deshalb räumt der Aufrufer hier auf
return err;
}
ERROR_IO_PENDING bedeutet „angenommen, noch nicht abgeschlossen“. Es darf nicht wie ein gewöhnlicher Fehler aufgeräumt werden. Der synchrone Abschluss mit TRUE ist ebenfalls ein normaler Weg und muss behandelt werden. Warum synchroner Abschluss vorkommt, erklärt Kapitel 5.
Verarbeiten Sie das Ergebnis genau einmal. Standardmäßig wird auch bei einem synchron abgeschlossenen Vorgang ein Abschlusspaket eingereiht, wenn das Handle mit einem IOCP verknüpft ist, und bei der Ereignismethode wird das Ereignis signalisiert. Verarbeiten Sie sowohl direkt nach TRUE als auch beim Eintreffen der Benachrichtigung, behandeln Sie denselben Vorgang doppelt und riskieren, die Struktur doppelt freizugeben. Die sichere Grundform ist, beide Wege — TRUE und ERROR_IO_PENDING — auf die Ergebnisverarbeitung auf dem Benachrichtigungsweg zu bündeln.1
Dagegen behandelt die ausgebende Stelle Fehlerbehandlung und Aufräumen auf dem Weg, auf dem die Ausgabe selbst fehlgeschlagen ist. Leiten Sie ihn ins Warten, obwohl keine Benachrichtigung kommt, warten Sie ewig.
Es gibt auch eine Optimierung, die die IOCP-Benachrichtigung bei synchronem Abschluss weglässt; das ist ein anderer Entwurf als das Standardverhalten. Den Geltungsbereich von FILE_SKIP_COMPLETION_PORT_ON_SUCCESS trennt Abschnitt 5.3.7
4. Die Abschlussbenachrichtigung wählen: nach der Zahl der I/Os und dem verarbeitenden Thread
Sobald Ausgabe und Abschluss getrennt sind, braucht es einen Weg, den Abschluss entgegenzunehmen. Vergleichen Sie zuerst die vier Methoden entlang von wie viele I/Os gleichzeitig und auf welchem Thread die Abschlussverarbeitung läuft.1
| Methode | Thread, auf dem die Abschlussverarbeitung läuft | Zahl gleichzeitig möglicher I/Os | Geeignet für |
|---|---|---|---|
| (1) Signalisierung des Handles | Ein beliebiger wartender Thread | Praktisch eines. Bei mehreren gleichzeitig lässt sich nicht unterscheiden, welches abgeschlossen ist | Fast nirgends (4.1) |
(2) Ereignis + GetOverlappedResult |
Ein beliebiger wartender Thread | Pro Vorgang ein Ereignis. Wartet man mit WaitForMultipleObjects gesammelt, liegt die Grenze bei 64 |
Bis zu wenigen gleichzeitigen I/Os. Gerätekommunikation (4.2) |
(3) APC (ReadFileEx) |
Der ausgebende Thread, und nur während er sich in einem alertable wait befindet | Keine Begrenzung der Anzahl, aber die gesamte Abschlussverarbeitung läuft seriell auf diesem einen Thread | Kommunikationsverarbeitung, die auf einem einzigen Thread bleiben soll (4.3) |
| (4) I/O-Abschlussport | Die dem Port zugeordnete Gruppe von Worker-Threads | Viele mit wenigen Threads bedienbar | Server, Threadpools (4.4) |
flowchart TB
DONE["I/O ist im Kernel abgeschlossen<br/>(IoCompleteRequest → Ergebnis wird über eine APC festgelegt)"]
N1["(1) Das Dateihandle wird signalisiert<br/>Empfang: WaitForSingleObject(Handle)"]
N2["(2) hEvent von OVERLAPPED wird signalisiert<br/>Empfang: WaitForSingleObject + GetOverlappedResult"]
N3["(3) Eine Abschlussroutine wird in die APC-Warteschlange des ausgebenden Threads gestellt<br/>Empfang: läuft während eines alertable wait wie SleepEx"]
N4["(4) Ein Abschlusspaket landet an einem I/O-Abschlussport<br/>Empfang: GetQueuedCompletionStatus (Teil 3)"]
DONE --> N1
DONE --> N2
DONE --> N3
DONE --> N4
Abbildung 4: Die vier Wege der Abschlussbenachrichtigung. Die Empfangsmethode hängt von der Art der Ausgabe ab
4.1. Signalisierung des Handles: Vorgänge lassen sich nicht unterscheiden
Geben Sie ohne gesetztes hEvent aus, wird bei Abschluss das Dateihandle selbst signalisiert. Laufen über dasselbe Handle mehrere Vorgänge, lässt sich aber nicht unterscheiden, welcher abgeschlossen ist.1
Abgesehen vom Sonderfall, asynchrones I/O immer nur einzeln auszugeben, ist es sicherer, darauf zu verzichten. Es wirkt bequem, ergibt aber keinen Mechanismus, Ergebnisse pro Vorgang zu verwalten.
4.2. Ereignis und GetOverlappedResult: die Grundform für wenige gleichzeitige I/Os
Setzen Sie in OVERLAPPED.hEvent jedes Vorgangs ein manuell zurückzusetzendes Ereignis und geben Sie aus. Nach dem Warten mit WaitForSingleObject holen Sie mit GetOverlappedResult Erfolg oder Misserfolg und die übertragene Bytezahl. Mehrere Ereignisse gemeinsam warten Sie mit WaitForMultipleObjects; gleichzeitig warten lassen sich höchstens 64.18
Setzen Sie bWait von GetOverlappedResult auf TRUE, können Sie auch bis zum Abschluss warten und dann das Ergebnis holen. Verwenden Sie hier ein automatisch zurücksetzendes Ereignis, kann GetOverlappedResult weiterwarten, nachdem ein anderes Warten das Signal konsumiert hat. Manuelles Zurücksetzen vermeiden Sie genau dieses Warteproblem.83
Für wenige gleichzeitige I/Os ist das ein übersichtlicher, solider Weg. Auch bei seriellen Ports, wenn „lesen während man schreibt“ nötig ist, kommt er zum Einsatz. Ein Praxisbeispiel steht in „Fallstricke bei seriellen Kommunikationsanwendungen“.
4.3. APC: den ausgebenden Thread bis zum Abschluss im alertable wait halten
ReadFileEx / WriteFileEx sind der Weg, bei dem Sie eine Abschlussroutine (einen Callback) angeben. Ist das I/O abgeschlossen, wird die Routine in die APC-Warteschlange des ausgebenden Threads gestellt. Sie läuft, wenn dieser Thread über SleepEx, WaitForSingleObjectEx oder ähnlich in einen alertable wait eintritt.91011
Weil die Abschlussverarbeitung seriell auf demselben Thread läuft, kann Verarbeitung, die auf einem einzigen Thread bleibt, Sperren vermeiden. Tritt der ausgebende Thread dagegen nie in einen alertable wait ein, läuft die Abschlussroutine nicht. Die Kombination mit der Nachrichtenschleife einer UI erfordert MsgWaitForMultipleObjectsEx; der Entwurf des Wartens wird schwieriger. Für den allgemeinen Fall werden oft Ereignis oder IOCP gewählt.
Bei APC übersieht man leicht drei Punkte: ob die Ausgabe gelungen ist, ob die Art des Wartens stimmt, und ob der eigene Vorgang abgeschlossen ist. Der folgende Code ist ein Auszug, der schlechtes und gutes Warten gegenüberstellt; er ist kein Beispiel, beide nacheinander auszuführen. Vorbereitung von Handle und Puffer sowie OnReadCompleted, das das Abschlussflag des Vorgangs setzt, werden woanders vorausgesetzt.
// C++ / Win32. hFile ist ein mit FILE_FLAG_OVERLAPPED geöffnetes Handle,
// ov und buf werden bis zum Abschluss am Leben gehalten (Abschnitt 3.2)
// Schlechtes Beispiel: Die Abschlussroutine wird niemals aufgerufen
ReadFileEx(hFile, buf, len, ov, OnReadCompleted);
Sleep(1000); // Kein alertable wait. Die APC wird nicht zugestellt
// Gutes Beispiel: alertable weiter warten, bis genau dieses I/O fertig ist
//
// Dieses Flag von der Abschlussroutine aus setzen (z. B. in einer Struktur gemeinsam mit ov gehalten)
volatile bool completed = false;
// Unbedingt prüfen, ob die Ausgabe selbst erfolgreich war. Rückgabewert 0 bedeutet,
// dass keine Abschlussroutine eingereiht wurde
if (!ReadFileEx(hFile, buf, len, ov, OnReadCompleted))
{
const DWORD err = GetLastError(); // Sofort abholen. Wird durch spätere APIs überschrieben
ReportError(err); // Gerät entfernt, ungültiges Handle usw.
return; // NICHT in die untere Warteschleife eintreten
}
while (!completed)
{
DWORD r = SleepEx(1000, TRUE); // TRUE als zweites Argument macht das Warten alertable
if (r == WAIT_IO_COMPLETION)
{
// Irgendeine APC wurde ausgeführt. Das muss aber nicht das eigene I/O sein,
// deshalb anhand von completed entscheiden und sonst weiterwarten
continue;
}
// Bei Timeout zurückgekehrt. Das I/O steht noch aus, deshalb bei Abbruch
// mit CancelIoEx abbrechen und auf die Zustellung des Abschlusses warten
CancelIoEx(hFile, ov);
}
Ist die Ausgabe fehlgeschlagen, treten Sie nicht ins Warten ein. Gibt ReadFileEx 0 zurück, weil das Gerät entfernt wurde, das Handle ungültig ist oder aus einem anderen Grund, wurde keine Abschlussroutine eingereiht. Holen Sie GetLastError() sofort, behandeln Sie den Fehler und kehren Sie zurück. Übersehen Sie das, wird completed nie wahr, und Sie wiederholen SleepEx und CancelIoEx gegen ein I/O, das nicht existiert.9
Behandeln Sie den Timeout des Wartens nicht als Ende des I/O. Kehrt SleepEx per Timeout zurück, verlässt der Thread den alertable wait, das ausgegebene I/O kann aber noch ausstehen. Verlassen Sie den Gültigkeitsbereich nicht und lassen ov oder buf ablaufen. Warten Sie weiter auf den Abschluss, oder fordern Sie bei Abbruch den Abbruch an und warten, bis dieser Abschluss zugestellt wird. Die Lebensdauerregel aus Abschnitt 3.2 gilt auch nach dem Timeout.
Schließen Sie allein aus WAIT_IO_COMPLETION nicht, dass das eigene I/O fertig ist. Dieser Rückgabewert bedeutet, dass mindestens eine APC ausgeführt wurde. Ist auf demselben Thread ein anderes I/O oder eine über QueueUserAPC eingereihte APC anhängig, kehrt man dafür ebenfalls zurück. Entscheiden Sie anhand des Flags, das die eigene Abschlussroutine setzt, und warten Sie sonst weiter.10
Wenn „die APC nicht kommt“, prüfen Sie neben dem Erfolg der Ausgabe die Wartfunktion. Ist es SleepEx(..., TRUE) statt Sleep, WaitForSingleObjectEx(..., TRUE) statt WaitForSingleObject? Das nachgestellte Ex und TRUE als alertable-Argument sind die Prüfpunkte.10
4.4. IOCP: viele I/Os mit wenigen Workern bedienen
Bei einem I/O-Abschlussport (IOCP) verknüpfen Sie das Handle mit einem Port. Abschlusspakete landen in der Warteschlange des Ports, Worker-Threads holen sie mit GetQueuedCompletionStatus ab. Das ist der Mechanismus, viele gleichzeitige I/Os mit wenigen Threads zu verarbeiten.12
Es ist auch der Weg, der asynchrones I/O in .NET trägt. Wie die Warteschlange der Abschlussbenachrichtigungen mit der Steuerung der Zahl gleichzeitig laufender Threads kombiniert wird, behandelt ausführlich Teil 3.
5. Die Ausnahme des synchronen Abschlusses: „asynchron“ und „wird nicht zum Warten gezwungen“ sind nicht dasselbe
5.1. Typische Bedingungen, unter denen der Abschluss innerhalb des Aufrufs erfolgt
Auch wenn Sie korrekt im asynchronen Modus ausgeben, kann das I/O innerhalb des Aufrufs abschließen. Synchroner Abschluss bedeutet, dass das I/O fertig war, bevor die Funktion zurückkehrte; das ist keine Garantie, dass sie schnell zurückkehrt. Halten Sie den Fall, der wegen eines Cache-Treffers schnell endet, getrennt von dem Fall, in dem Sie innerhalb des Aufrufs warten müssen.2
flowchart TB
A["ReadFile/WriteFile an ein asynchrones Handle ausgeben"]
Q{"Trifft eine Bedingung für synchronen Abschluss zu?"}
C1["Sofort erfüllbare Anfrage<br/>(z. B. Daten liegen im Cache)"]
C2["NTFS-komprimierte Datei<br/>(komprimierte Dateien werden nicht asynchron)"]
C3["NTFS-verschlüsselte (EFS) Datei"]
C4["Schreibvorgang, der die Dateilänge verlängert"]
T["Kehrt sofort mit TRUE zurück<br/>= wurde innerhalb des Aufrufs bis zum Abschluss ausgeführt"]
P["Kehrt mit ERROR_IO_PENDING zurück<br/>= läuft tatsächlich asynchron"]
A --> Q
Q --> C1
Q --> C2
Q --> C3
Q --> C4
C1 --> T
C2 --> T
C3 --> T
C4 --> T
Q -->|"keine davon"| P
Abbildung 5: Die wichtigsten Bedingungen, unter denen asynchron ausgegebenes I/O synchron abschließt. Das ist getrennt von der Zeit, bis der Aufruf zurückkehrt
Microsofts Dokument zur Fehlerbehebung nennt die folgenden Gründe.2
| Bedingung | Warum synchron verarbeitet wird, und was das für den Code bedeutet |
|---|---|
| Sofort erfüllbare Anfrage, Cache-Treffer | Liegen die Daten im Speicher, kann der Treiber an Ort und Stelle abschließen. Schnell fertig zu sein ist in Ordnung, Code der voraussetzt, dass immer ERROR_IO_PENDING kommt, bricht aber |
| Cache-fähiges Lesen, bei dem die benötigte Seite fehlt | Der Windows-Cache ist über File-Mapping implementiert. Es gibt keinen asynchronen Seitenfehlermechanismus, deshalb kann synchron verarbeitet werden |
| NTFS-komprimierte oder EFS-verschlüsselte Datei | Der Dateisystemtreiber wandelt den Zugriff in synchronen Zugriff um |
| Schreibvorgang, der die Datei verlängert | Ein Schreibvorgang, der die Länge ändert, wird synchron |
Wichtig ist, dass synchrone Verarbeitung nicht nur beim Cache-Treffer, sondern auch dann auftreten kann, wenn die Daten nicht im Cache liegen. Den Cache-Mechanismus selbst behandelt Teil 4 der Serie.
5.2. Die Verzweigung des Ausgabeergebnisses und die Reaktionsfähigkeit der UI getrennt entwerfen
Zuerst müssen alle drei Zweige aus Abschnitt 3.3 behandelt werden. Behandeln Sie auch TRUE als normales Ergebnis und bündeln Sie die Ergebnisverarbeitung standardmäßig auf dem Benachrichtigungsweg.
Die Zweige korrekt zu schreiben garantiert aber keine Reaktionsfähigkeit. „Es ist asynchrones I/O, also friert die UI nicht ein“ gilt nicht. Threads, die niemals stehen bleiben dürfen, brauchen einen Entwurf, der die Ausgabe des I/O selbst auslagert, auf einen eigenen Thread oder einen Threadpool. Die zugehörige Praxis steht auch in „Praktischer Leitfaden, um auf gewöhnlichem Windows so nah wie möglich an Soft Real-Time heranzukommen“.
5.3. Das Weglassen der Benachrichtigung bei synchronem Abschluss ist eine Optimierung nur für IOCP
Bei hochfrequentem I/O lässt sich die Benachrichtigung bei synchronem Abschluss weglassen. Aktivieren Sie über SetFileCompletionNotificationModes FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, wird für sofort erfolgreiches I/O kein Abschlusspaket an den IOCP gestellt. Das ist die Angabe, wenn Sie auf einen Entwurf umschalten, der das Ergebnis an Ort und Stelle verarbeitet statt auf dem Benachrichtigungsweg.7
Weggelassen wird nur das Paket an den IOCP: Die Signalisierung von OVERLAPPED.hEvent wird nicht unterdrückt. Wenden Sie dieselbe Optimierung nicht auf den Ereignisweg an. Mischen Sie den Standard-Benachrichtigungsweg mit dem optimierten Weg, entstehen die doppelte Verarbeitung aus Abschnitt 3.3 oder Fehler beim Warten auf eine Benachrichtigung. Die Kombination mit IOCP behandelt Teil 3.
6. Abbruch und Beendigung: anfordern, den Abschluss bestätigen, schließen
6.1. Die API nach dem Abbruchziel wählen
Die APIs zum Abbrechen werden nach Vorgang und ausgebendem Thread gewählt.4135
| API | Ziel und Angabe |
|---|---|
CancelIoEx |
Fordert unabhängig vom ausgebenden Thread den Abbruch nicht abgeschlossener I/Os eines angegebenen Handles an. Ein OVERLAPPED im zweiten Argument trifft diesen Vorgang, NULL alle Vorgänge des Handles |
CancelIo |
Trifft nur Vorgänge, die der aufrufende Thread selbst ausgegeben hat |
CancelSynchronousIo |
Trifft ein auf einem angegebenen anderen Thread laufendes synchrones I/O |
CancelIoEx wurde in Vista eingeführt. Für asynchrones I/O gibt es heute keinen Grund, absichtlich das ältere CancelIo mit seiner Einschränkung auf den ausgebenden Thread zu verwenden; nehmen Sie CancelIoEx als Grundlage.
6.2. Erfolg von CancelIoEx bedeutet nicht das Ende des I/O
CancelIoEx ist eine API, die den Abbruch nicht abgeschlossener IRPs anfordert, keine API, die auf den Abschluss des Vorgangs wartet. Erfolg bedeutet nur, dass der Abbruch angefordert wurde. Ein Vorgang, der bereits kurz vor dem Abschluss stand, kann normal abschließen, weil der Abbruch nicht rechtzeitig greift.14
sequenceDiagram
participant App as Anwendung
participant IOM as I/O-Manager
participant DRV as Treiber
App->>IOM: CancelIoEx(Handle, OVERLAPPED)
Note over IOM: Fordert einen Abbruch für das betreffende<br/>nicht abgeschlossene IRP an (markiert es)
IOM->>DRV: Ruft die Abbruchroutine auf
Note over DRV: Bricht ab, wenn der Zustand das zulässt<br/>kurz vor Abschluss kann er auch normal abschließen
DRV->>IOM: IoCompleteRequest<br/>(STATUS_CANCELLED)
IOM-->>App: Die Abschlussbenachrichtigung trifft ein<br/>GetOverlappedResult meldet ERROR_OPERATION_ABORTED
Note over App: Erst nachdem diese Benachrichtigung gesehen wurde,<br/>OVERLAPPED und Puffer freigeben
Abbildung 6: Auch ein abgebrochener Vorgang wird als Abschluss gemeldet. Aufgeräumt wird nach dieser Bestätigung
Ein tatsächlich abgebrochener Vorgang kommt in der Abschlussbenachrichtigung als ERROR_OPERATION_ABORTED zurück. Ob normal abgeschlossen oder abgebrochen: Geben Sie Struktur und Puffer nicht frei, bevor die Benachrichtigung eingetroffen ist. Geben Sie sie vorher frei, verliert der Kernel einen Bereich, den er noch verwendet, und es kommt zu Speicherbeschädigung. Bei einer Zugriffsverletzung nach dem Abbruch prüfen Sie zuerst diese Lebensdauer.414
6.3. Ausgegebene Vorgänge einziehen, bevor das Handle geschlossen wird
Die Grundform der Beendigung ist Abbruch anfordern → Abschluss abwarten → Handle schließen.
Wie in Teil 1 gesehen, stößt das Schließen des letzten Handles in der Cleanup-Verarbeitung den Abbruch nicht abgeschlossener IRPs an. Schließen Sie aber nur das Handle, während ausgegebenes I/O noch aussteht, gerät die Verwaltung von Abschlussbenachrichtigung und Pufferlebensdauer leicht durcheinander. Überlassen Sie das Aufräumen nicht dem Schließen; räumen Sie nicht abgeschlossene Vorgänge zuerst ab.
Dasselbe gilt, wenn Sie Verarbeitung nach einem Timeout abbrechen wollen. Das Betriebssystem entscheidet die Abbruchkriterien der Anwendung nicht für Sie, deshalb entwerfen Sie Abbruch nach Timeout und das Entgegennehmen des Abschlusses als Satz. Beim APC-Weg halten Sie den alertable wait bis zur Zustellung des Abschlusses aufrecht, wie in Abschnitt 4.3.
7. Die Entsprechung in .NET: nicht nur ReadAsync, sondern die Stelle, an der die Datei geöffnet wird
7.1. Den Modus des Handles und die aufgerufene API abstimmen
useAsync von FileStream beziehungsweise FileOptions.Asynchronous entspricht FILE_FLAG_OVERLAPPED in Win32. Wie in der Entsprechungstabelle in Teil 1 ist auch in .NET der Modus beim Öffnen der Datei entscheidend.1516
flowchart TB
A["await fs.ReadAsync(...)"]
Q{"Ist das Handle im asynchronen Modus<br/>(FileOptions.Asynchronous)?"}
Y["Echtes asynchrones I/O<br/>Gibt etwas OVERLAPPED-Äquivalentes aus<br/>Der Abschluss erreicht den Threadpool über IOCP (Teil 3)"]
N["Vorgetäuschte Asynchronität<br/>Ein Thread aus dem Threadpool<br/>übernimmt ein synchrones Read und wartet"]
A --> Q
Q -->|Ja| Y
Q -->|Nein| N
Abbildung 7: Beim selben ReadAsync ändert der Modus des Handles den Verarbeitungsweg auf der OS-Seite
| Kombination aus Handle und API | Was intern geschieht |
|---|---|
Asynchroner Modus + ReadAsync / WriteAsync |
Die Kombination, die asynchrones I/O des Betriebssystems verwendet |
Synchroner Modus + ReadAsync / WriteAsync |
Ein Thread aus dem Threadpool übernimmt das synchrone Lesen und Schreiben: vorgetäuschte Asynchronität |
Asynchroner Modus + synchrones Read / Write |
Es entsteht Overhead durch internes Warten auf den Abschluss |
Auch bei vorgetäuschter Asynchronität wartet der aufrufende Thread nicht, aber im Hintergrund wartet ein anderer Thread. Bei wenigen Vorgängen ist der reale Schaden begrenzt, auf einem Server oder bei hochfrequenter Verarbeitung wird es zur Ursache für Erschöpfung des Threadpools und geringere Skalierbarkeit. Modus und API abzustimmen ist der Grundsatz.1615
7.2. Drei Wege, einen FileStream zu erzeugen, vergleichen
In (A) und (B) unten ist der Aufruf von ReadAsync derselbe. Der Unterschied liegt nur in useAsync beim Öffnen der Datei. (C) ist das Beispiel ab .NET 6, das Handle und Position explizit macht.
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) Vorgetäuschte Asynchronität. useAsync weglassen oder false setzt das Handle in den synchronen Modus
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: false))
{
// Der Aufrufer wird nicht blockiert, aber im Hintergrund übernimmt ein Threadpool-Thread ein synchrones Read und wartet
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (B) Echtes asynchrones I/O. useAsync: true bildet sich direkt auf FILE_FLAG_OVERLAPPED ab
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true))
{
// Der Abschluss erreicht den Threadpool über IOCP (Teil 3)
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (C) Ab .NET 6. Eine unmittelbare Schreibweise mit explizitem Modus und Offset
using (SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read,
options: FileOptions.Asynchronous))
{
int read = await RandomAccess.ReadAsync(handle, buffer, fileOffset: 0);
}
Beim Durchsehen bestehenden Codes suchen Sie nicht nur die Aufrufe von ReadAsync / WriteAsync, sondern die Stellen, an denen ein FileStream erzeugt wird. Kurze Überladungen wie File.OpenRead oder new FileStream(path, FileMode.Open) öffnen im synchronen Modus. Erzeugen Sie einen FileStream aus einem SafeFileHandle, stimmen Sie das Argument isAsync auf den tatsächlichen Modus des Handles ab.
7.3. Bei RandomAccess Handle und Offset explizit angeben
In .NET 6 wurde die interne Implementierung von FileStream vollständig neu geschrieben, und File.OpenHandle sowie RandomAccess kamen hinzu. Es sind APIs, die ein SafeFileHandle direkt verwenden und die Lese- und Schreibposition jedes Mal übergeben.16
Die Form in (C), die Modus und fileOffset explizit macht, entspricht der in diesem Artikel beschriebenen Aufteilung: asynchrones Handle und OVERLAPPED.Offset pro Vorgang.
7.4. Auch mit CancellationToken bleibt der Abbruch eine Anforderung
Bei einem Handle im asynchronen Modus landet der Abbruch von Datei-I/O über CancellationToken intern bei CancelIoEx. Hinter einem ReadAsync, dem ein Token übergeben wurde und das mit einer OperationCanceledException endet, arbeitet der Mechanismus aus Kapitel 6. Dass ein sofortiger Abbruch nicht garantiert ist, gilt ebenso.
Bei der vorgetäuschten Asynchronität im synchronen Modus gibt es keinen Overlapped-Vorgang als Abbruchziel, dieser Weg steht also nicht zur Verfügung. Neuere .NET-Laufzeiten enthalten auch einen Mechanismus, der für synchron laufende Aufrufe einen Abbruch über CancelSynchronousIo versucht; das Verhalten hängt von Laufzeitversion und Vorgangsart ab und garantiert keinen zuverlässigen Abbruch. Soll der Abbruch Teil des Entwurfs sein, ist der richtige Weg, den Modus des Handles abzustimmen und asynchrones I/O des Betriebssystems zu verwenden.
Die Praxis eine Ebene über async/await — etwa ConfigureAwait und die Beziehung zum UI-Thread — steht in „C# async/await-Praxis-Entscheidungstabelle - Task.Run und ConfigureAwait“ und „WPF/WinForms: async und der UI-Thread auf einem Blatt“. Dieser Artikel erklärt, wie das Betriebssystem darunter das Lesen und Schreiben voranbringt.
8. Zusammenfassung: von der Ausgabe bis zum Aufräumen als einen Zusammenhang prüfen
Beim Prüfen von asynchronem I/O folgen Sie dem Code in dieser Reihenfolge.
- An der Öffnungsstelle den synchronen oder asynchronen Modus prüfen. Bei einer Datei auf dem Datenträger die Position bei jedem asynchronen Vorgang angeben.
- An der Ausgabestelle prüfen, dass es ein vorgangseigenes
OVERLAPPEDund einen Puffer gibt und dass die drei Zweige aus Abschnitt 3.3 behandelt werden. - An der Stelle, die den Abschluss entgegennimmt, prüfen, dass die Art des Wartens zu Ereignis, APC oder IOCP passt und dass dasselbe Ergebnis nicht doppelt verarbeitet wird.
- An der Beendigungsstelle prüfen, dass nicht allein aufgrund von Timeout oder Abbruchanforderung freigegeben wird.
Synchrones I/O und asynchrones I/O sind keine getrennten Leitungen. Der Unterschied liegt darin, ob nach dem Warten auf den Abschluss zurückgekehrt wird oder ein Weg genutzt wird, der vor dem Abschluss zurückkehrt. Weil auch im asynchronen Modus synchroner Abschluss vorkommt, trennen Sie die Verzweigung des Ausgabeergebnisses vom Entwurf der Reaktionsfähigkeit.12
Der Modus gehört zum Handle, der Zustand zu jedem Vorgang, und aufgeräumt wird erst, nachdem der Abschluss bestätigt ist. Diese Aufteilung gilt sowohl, wenn Sie Win32-OVERLAPPED direkt verwenden, als auch, wenn Sie FileOptions.Asynchronous in .NET nutzen. Auch ein abgebrochener Vorgang bleibt in der Verwaltung, bis Sie den Abschluss entgegennehmen.3415
Weiter geht es in Teil 3, „I/O-Abschlussports (IOCP) und der .NET-Threadpool — Der Keller unter async/await“. Dort geht es darum, warum der IOCP aus Abschnitt 4.4 die Warteschlange der Abschlussbenachrichtigungen und die Steuerung der Zahl ausführender Threads vereint, und auf welchem Thread die Fortsetzung eines await läuft.
Verwandte Artikel
- Die Tiefen von Windows I/O (Teil 1) — Jedes Lesen und Schreiben wird zu einem IRP: Das Gesamtbild des I/O-Systems
- C# async/await-Praxis-Entscheidungstabelle - Task.Run und ConfigureAwait
- WPF/WinForms: async und der UI-Thread auf einem Blatt
- Fallstricke bei seriellen Kommunikationsanwendungen - von der Wiederverbindung bis zum Log-Design
- Warum Sie unter Windows Ereigniswarten gegenüber Sleep(1) bevorzugen sollten
- Praktischer Leitfaden, um auf gewöhnlichem Windows so nah wie möglich an Soft Real-Time heranzukommen
- Der Irrglaube, man könne über TCP pro Send genau eine Einheit empfangen — Empfangslogik entwerfen, die es als Bytestrom behandelt
Verwandte Beratungsleistungen
Die KomuraSoft LLC übernimmt den Entwurf von Windows-Geschäftsanwendungen und Gerätekommunikationsanwendungen, die asynchrones I/O verwenden, sowie die Ursachenermittlung bei Störungen wie Einfrieren, Absturz beim Abbruch oder Erschöpfung des Threadpools.
- Windows-Anwendungsentwicklung
- Fehleranalyse und Ursachenermittlung
- Entwicklung von Soft-Realtime-Windows-Anwendungen
- Kontakt
Quellen
-
Microsoft Learn, Synchronous and asynchronous I/O. Darüber, dass bei synchronem I/O die Funktion bis zum I/O-Abschluss blockiert, während bei asynchronem I/O die ausgebende Funktion sofort zurückkehrt und der Thread mit anderer Arbeit fortfahren kann; dass asynchrones I/O ein mit FILE_FLAG_OVERLAPPED geöffnetes Handle erfordert; über die Benachrichtigungsmethoden für den Abschluss — Signalisierung des Dateihandles, Signalisierung des in der OVERLAPPED-Struktur angegebenen Ereignisses, eine während eines alertable wait ausgeführte Abschlussroutine (APC) und I/O-Abschlussports; und darüber, dass die Signalisierung des Dateihandles nicht unterscheiden kann, welcher Vorgang abgeschlossen ist, wenn mehrere Vorgänge gleichzeitig laufen. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Über die Gründe, warum für asynchrones Verhalten codiertes I/O trotzdem synchron abschließt: eine NTFS-komprimierte Datei (der Dateisystemtreiber greift nicht asynchron auf komprimierte Dateien zu, sodass alle Vorgänge synchron werden), eine NTFS-verschlüsselte Datei, ein Schreibvorgang, der die Dateilänge verlängert, sowie der Fall, dass der Treiber den Vorgang bei sofort erfüllbarer Anfrage (etwa wenn die Daten bereits im Arbeitsspeicher-Cache liegen) direkt abschließt und TRUE zurückgibt; darüber, dass Windows’ Cache über File-Mapping implementiert ist und bei fehlender Seite kein asynchroner Seitenfehlermechanismus existiert; und zusätzlich darüber, dass für drei ausgegebene I/Os drei OVERLAPPED-Strukturen benötigt werden, Wiederverwendung zu unvorhersehbaren Ergebnissen oder Datenbeschädigung führt, und dass der zugehörige Datenpuffer bis zum Abschluss des Vorgangs nicht gelesen oder geschrieben werden darf. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, OVERLAPPED structure. Darüber, dass die OVERLAPPED-Struktur Informationen für asynchrone Ein- und Ausgabe hält; dass Offset/OffsetHigh die Dateiposition, hEvent ein bei Abschluss signalisiertes Ereignis und Internal/InternalHigh den Statuscode des Vorgangs sowie die übertragene Bytezahl halten; dass die Struktur während der Ausführung des Vorgangs gültig und unverändert gehalten werden muss; und über Hinweise bei der Verwendung des Ereignisses. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, CancelIoEx function. Darüber, dass CancelIoEx nicht abgeschlossenes I/O eines angegebenen Handles zum Abbruch markiert, unabhängig davon, welcher Thread es ausgegeben hat; dass die Angabe von lpOverlapped nur diesen einen Vorgang trifft, NULL dagegen alle nicht abgeschlossenen I/Os; dass ein abgebrochener Vorgang mit ERROR_OPERATION_ABORTED abschließt; und dass der Abbruch aller Vorgänge nicht garantiert ist, weshalb man warten muss, bis die Abschlussverarbeitung erledigt ist. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CancelSynchronousIo function. Darüber, dass CancelSynchronousIo einen von einem angegebenen Thread ausgeführten synchronen I/O-Vorgang zum Abbruch markiert, und dass der abgebrochene Vorgang als Fehlschlag mit ERROR_OPERATION_ABORTED zurückgegeben wird. ↩ ↩2
-
Microsoft Learn, ReadFile function. Darüber, dass lpOverlapped bei einem mit FILE_FLAG_OVERLAPPED geöffneten Handle erforderlich ist und die Leseanfangsposition über Offset/OffsetHigh der OVERLAPPED-Struktur angegeben wird; dass bei asynchroner Verarbeitung FALSE und ERROR_IO_PENDING zurückgegeben werden; dass das System für ein asynchrones Handle keinen Dateizeiger führt; und dass beim Übergeben eines OVERLAPPED an ein ohne FILE_FLAG_OVERLAPPED geöffnetes Handle zwar ab dem angegebenen Offset gelesen wird, ReadFile aber weiterhin erst nach Abschluss des Lesevorgangs zurückkehrt. ↩ ↩2 ↩3
-
Microsoft Learn, SetFileCompletionNotificationModes function. Darüber, dass FILE_SKIP_COMPLETION_PORT_ON_SUCCESS erlaubt, bei sofort erfolgreichem I/O das Einreihen eines Abschlusspakets an einem I/O-Abschlussport zu überspringen, und dass FILE_SKIP_SET_EVENT_ON_HANDLE erlaubt, das Setzen des Ereignisses des Dateihandles zu überspringen. ↩ ↩2
-
Microsoft Learn, GetOverlappedResult function. Darüber, dass GetOverlappedResult das Ergebnis eines asynchronen Vorgangs (Erfolg/Misserfolg und übertragene Bytezahl) liefert; dass die Übergabe von TRUE für bWait auf den Abschluss des Vorgangs wartet; und darüber, dass bei einem automatisch zurücksetzenden Ereignis in OVERLAPPEDs hEvent, das von einem anderen Warten konsumiert wird, ein Aufruf mit bWait=TRUE den Abschluss möglicherweise nicht erkennt und hängen bleibt, weshalb ein manuell zurückzusetzendes Ereignis verwendet werden sollte. ↩ ↩2
-
Microsoft Learn, ReadFileEx function. Darüber, dass ReadFileEx eine beim Abschluss des Lesevorgangs aufgerufene Abschlussroutine (FileIOCompletionRoutine) entgegennimmt; dass die Abschlussroutine läuft, wenn sich der aufrufende Thread in einem alertable-wait-Zustand befindet; und dass ein mit FILE_FLAG_OVERLAPPED geöffnetes Handle erforderlich ist. ↩ ↩2
-
Microsoft Learn, Alertable I/O. Darüber, dass bei alertable I/O ein Eintrag für die Abschlussroutine in die APC-Warteschlange des Threads gestellt wird; dass die APC ausgeführt wird, wenn der Thread über SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx oder ähnlich in einen alertable Zustand eintritt; und dass eine APC immer im Kontext des Threads ausgeführt wird, der sie ausgelöst hat. ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous Procedure Calls. Darüber, dass eine APC eine Funktion ist, die asynchron im Kontext eines bestimmten Threads ausgeführt wird; dass jeder Thread seine eigene APC-Warteschlange besitzt; und dass eine Benutzermodus-APC nur ausgeführt wird, wenn sich der Thread in einem alertable Zustand befindet. ↩
-
Microsoft Learn, I/O Completion Ports. Darüber, dass ein I/O-Abschlussport ein effizientes Threading-Modell zur Verarbeitung vieler asynchroner I/O-Anfragen auf einem Mehrprozessorsystem bereitstellt; dass durch die Verknüpfung eines Dateihandles mit einem Port Abschlusspakete in eine Warteschlange gestellt werden, die Worker-Threads mit GetQueuedCompletionStatus abholen; und dass der Port die Zahl gleichzeitig laufender Threads steuert. ↩
-
Microsoft Learn, CancelIo function. Darüber, dass CancelIo nur I/O-Vorgänge abbrechen kann, die der aufrufende Thread selbst ausgegeben hat, und dass zum Abbrechen von auch durch andere Threads ausgegebenen Vorgängen CancelIoEx zu verwenden ist. ↩
-
Microsoft Learn, Canceling pending I/O operations. Über den Mechanismus zum Abbrechen nicht abgeschlossener I/Os; darüber, dass ein Vorgang trotz angeforderten Abbruchs bereits auf dem Weg zum Abschluss sein kann; dass der Abschluss eines abgebrochenen Vorgangs bestätigt werden sollte, bevor Ressourcen freigegeben werden; und über die Aufteilung, für synchrone Vorgänge CancelSynchronousIo und für asynchrone Vorgänge CancelIo/CancelIoEx zu verwenden. ↩ ↩2
-
Microsoft Learn, Asynchronous file I/O (.NET). Über das Konzept hinter asynchronem Datei-I/O in .NET; darüber, dass useAsync (FileOptions.Asynchronous) im Konstruktor von FileStream angegeben wird, um asynchrones I/O auf Betriebssystemebene zu aktivieren; und über die Unterscheidung zwischen der Verwendung synchroner und asynchroner Methoden. ↩ ↩2 ↩3
-
Microsoft .NET Blog, File IO improvements in .NET 6. Darüber, dass die interne Implementierung von FileStream in .NET 6 vollständig neu geschrieben wurde; dass sich die Strategie danach richtet, ob das Handle im asynchronen Modus geöffnet ist; dass File.OpenHandle ein SafeFileHandle direkt liefert und RandomAccess threadsicheres Lesen und Schreiben mit explizitem Offset ermöglicht; und dass asynchrone Aufrufe an ein nicht im asynchronen Modus geöffnetes Handle an den Threadpool ausgelagert werden. ↩ ↩2 ↩3
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Die Tiefen von Windows I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile den Datenträger?
Teil 4 einer bebilderten Reihe über den Windows-Cache-Manager. Behandelt den als Dateizuordnung umgesetzten Cache, Read-Ahead und verzöge...
Die Tiefen von Windows I/O (Teil 1) — Jedes Lesen und Schreiben wird zu einem IRP: Das Gesamtbild des I/O-Systems
Teil 1 einer Serie, die das Windows-I/O-System von Grund auf erklärt. Wir stellen den Namensraum des Object Managers, die drei Objektarte...
Die Tiefen von Windows I/O (Teil 3) — I/O-Completion-Ports (IOCP) und der .NET-Threadpool: Der Keller unter async/await
Teil 3 einer bebilderten Reihe über I/O-Completion-Ports (IOCP). Behandelt das Design, das Completion-Warteschlange und Threadanzahl-Steu...
Die Tiefen von Windows I/O (Teil 6, Abschluss) — Wie Minifilter funktionieren und langsame I/O mit Procmon untersuchen
Erklärt, wie Minifilter Datei-I/O überwachen und steuern: FltMgr, Altitudes, Pre-/Post-Callbacks, fltmc, langsame Vorgänge in Procmon fin...
Die Tiefen von Windows I/O (Teil 5) — NTFS-Interna: Das Dateisystem anhand des MFT verstehen
Teil 5 einer bebilderten Reihe über die NTFS-Interna. Behandelt werden MFT und Dateidatensätze, mehrere Datenströme (Zone.Identifier), Ha...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ändert sich, wenn man FILE_FLAG_OVERLAPPED setzt?
- Das Dateiobjekt hinter dem Handle wird im asynchronen Modus geöffnet. Das ist eine Eigenschaft pro Handle, die im Moment von CreateFile feststeht; zwischen synchron und asynchron lässt sich nicht pro Aufruf umschalten. Bei einem Handle im asynchronen Modus müssen Sie ReadFile/WriteFile immer eine OVERLAPPED-Struktur übergeben. Das System führt für dieses Handle keinen Dateizeiger (aktuelle Position). Bei Geräten mit Position, etwa einer Datei auf dem Datenträger, geben Sie die Lese- und Schreibposition deshalb jedes Mal über Offset in OVERLAPPED an (bei Geräten ohne Positionsbegriff, etwa seriellen Ports, wird Offset nicht verwendet). Ein ausgegebener Vorgang kann die Kontrolle zurückgeben, bevor er abgeschlossen ist; dann gibt ReadFile FALSE zurück, und GetLastError meldet ERROR_IO_PENDING. Den Abschluss erhalten Sie über eine Benachrichtigung, etwa ein Ereignis, eine APC oder einen I/O-Abschlussport.
- Warum kommt ein asynchron ausgegebenes I/O sofort abgeschlossen zurück?
- Asynchroner Modus bedeutet, dass Sie nicht auf den Abschluss warten müssen, nicht dass Sie niemals warten müssen. Microsofts Dokumentation nennt als typische Gründe, warum ein asynchron ausgegebener Vorgang trotzdem synchron abschließt: eine sofort erfüllbare Anfrage (etwa wenn die Daten bereits im Cache liegen), eine NTFS-komprimierte Datei, eine NTFS-verschlüsselte (EFS) Datei und ein Schreibvorgang, der die Dateilänge verlängert. In diesen Fällen geben ReadFile/WriteFile TRUE zurück, und das Ergebnis steht bereits fest. Code, der asynchrones I/O verwendet, muss deshalb sowohl den Fall mit ERROR_IO_PENDING als auch den Fall des sofortigen Abschlusses vorsehen; Reaktionsfähigkeit ist damit nicht absolut garantiert. Standardmäßig trifft auch bei einem synchron abgeschlossenen Vorgang zusätzlich eine Abschlussbenachrichtigung ein (ein signalisiertes Ereignis oder ein Paket an einem I/O-Abschlussport). Am sichersten ist es, die Ergebnisverarbeitung allein über den Benachrichtigungsweg zu bündeln.
- Darf man eine OVERLAPPED-Struktur wiederverwenden?
- Nicht für mehrere gleichzeitig laufende Vorgänge. Eine OVERLAPPED-Struktur repräsentiert den Zustand eines einzelnen, gerade laufenden Vorgangs. Microsofts Dokumentation hält ausdrücklich fest, dass für drei ausgegebene I/O-Vorgänge drei OVERLAPPED-Strukturen nötig sind und eine Wiederverwendung zu unvorhersehbaren Ergebnissen oder Datenbeschädigung führt. Bis ein Vorgang abgeschlossen ist, müssen Struktur und Lese-/Schreibpuffer gültig bleiben; ihr Inhalt darf nicht angefasst werden. Wollen Sie eine Struktur nach dem Abschluss erneut verwenden, initialisieren Sie sie jedes Mal neu, damit Reste der vorherigen Nutzung keine Wirkung mehr haben. Für hEvent ist ein manuell zurückzusetzendes Ereignis die sichere Wahl.
- Wie bricht man ein laufendes I/O ab?
- Mit CancelIoEx lässt sich unabhängig davon, welcher Thread den Vorgang ausgegeben hat, ein Abbruch für die nicht abgeschlossenen I/Os eines angegebenen Handles anfordern. Übergeben Sie im zweiten Argument eine OVERLAPPED-Struktur, um nur diesen einen Vorgang zu treffen, oder NULL, um alle Vorgänge dieses Handles zu treffen. Das ältere CancelIo kann nur Vorgänge abbrechen, die der aufrufende Thread selbst ausgegeben hat. Wichtig: Ein Abbruch ist eine Anforderung, keine sofortige Garantie. Ein Vorgang, der bereits kurz vor dem Abschluss stand, kann trotzdem normal abschließen, und ein abgebrochener Vorgang wird als Abschluss mit ERROR_OPERATION_ABORTED gemeldet. In beiden Fällen dürfen Sie OVERLAPPED-Struktur und Puffer erst freigeben, wenn die Abschlussbenachrichtigung eingetroffen ist. Für einen anderen Thread, der in synchronem I/O feststeckt, gibt es die eigene API CancelSynchronousIo.
- Was passiert, wenn man bei FileStream in .NET FileOptions.Asynchronous (useAsync) nicht angibt?
- Das Handle wird im synchronen Modus geöffnet. ReadAsync/WriteAsync ergeben dann kein echtes asynchrones I/O: Ein Thread aus dem Threadpool übernimmt das synchrone Lesen und Schreiben — vorgetäuschte Asynchronität. Der aufrufende Thread wird nicht blockiert, aber im Hintergrund wartet ein anderer Thread; das kann den Threadpool erschöpfen und die Skalierbarkeit senken. Öffnen Sie umgekehrt im asynchronen Modus und rufen dann das synchrone Read/Write auf, entsteht intern Overhead durch das Warten auf den Abschluss. Der Grundsatz lautet, den Modus des Handles und die aufgerufene API aufeinander abzustimmen. Ab .NET 6 erlauben File.OpenHandle und RandomAccess eine unmittelbare Schreibweise mit explizitem Modus und Offset.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.