Historial de revisiones (primera versión, publicada el 29 Jul 2026)
- Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22175268)
Los DOI siguientes remiten a versiones ya archivadas y pueden diferir del texto actual. Para citar el texto actual, utilice la URL de esta página.
Go Komura (2026). Las profundidades de la E/S de Windows (parte 2) ── E/S síncrona y asíncrona: el verdadero significado de OVERLAPPED. KomuraSoft LLC. https://comcomponent.com/es/blog/windows-io-sync-async-overlapped/
- DOI (archivo registrado)
- 10.5281/zenodo.22175268
- DOI (última versión registrada)
- 10.5281/zenodo.22175269
La vez pasada (parte 1) vimos que una solicitud de E/S de Windows se convierte en un paquete llamado IRP que recorre la pila de dispositivos, y que la emisión de la solicitud y su finalización están separadas dentro del núcleo. En esta entrega tratamos la E/S asíncrona (E/S superpuesta), que es cómo una aplicación usa esa separación.
Se añade FILE_FLAG_OVERLAPPED y aun así la llamada espera. Se reutiliza un OVERLAPPED y los datos se corrompen. Se libera el búfer justo después de cancelar y el proceso se cae. La clave para entenderlo es esta división del trabajo: «el modo pertenece al handle, el estado a cada operación, y la limpieza solo después de confirmar la finalización».
Este artículo sigue el recorrido en orden: abrir el archivo, emitir la E/S, recibir el resultado y hacer la limpieza. Una vez asentado el mecanismo de Win32, comprobamos dónde encajan FileStream, ReadAsync y CancellationToken de .NET.
Es la segunda entrega de la serie «Las profundidades de la E/S de Windows». La estructura completa está al comienzo de la parte 1.
1. Primero la conclusión: tres distinciones fáciles de confundir
La E/S asíncrona se vuelve difícil de entender si se juzga solo por el nombre de la API. Empiece por separar qué se configura de en qué momento se decide la finalización.
| Fácil de confundir | Cómo distinguirlos |
|---|---|
| El modo del handle y el estado de la operación | El modo síncrono o asíncrono se decide en el momento de CreateFile. OVERLAPPED guarda el estado de una sola operación emitida contra ese handle |
| El resultado de emitir y la recepción de la finalización | ERROR_IO_PENDING no es un fallo, sino una aceptación. TRUE es una finalización síncrona, pero por defecto también llega una notificación. No trate el resultado en ambos sitios |
| La solicitud de cancelación y el momento en que se puede limpiar | CancelIoEx es una solicitud de anulación. La estructura y el búfer se liberan después de confirmar la finalización de esa operación |
La E/S síncrona no vuelve al llamador antes de completarse. En la E/S asíncrona se puede usar una vía que vuelve antes de la finalización. Sin embargo, incluso en modo asíncrono la operación puede completarse dentro de la llamada, así que no hay una garantía de que «nunca se le hará esperar».12
En la implementación, piense en este orden: decidir el modo → preparar una estructura y un búfer exclusivos de la operación → juzgar el resultado de emitir → recibir la finalización → hacer la limpieza. Aunque haya solicitado la cancelación, no omita la etapa de recibir la finalización.34
Si ya tiene un objetivo concreto, avance desde la guía siguiente.
| Qué quiere saber o en qué está atascado | Dónde leer primero |
|---|---|
| En qué se diferencian la E/S síncrona y la asíncrona | Capítulo 2: el mecanismo de espera, apartado 3.1: el modo del handle |
| OVERLAPPED corrompe los datos, o el proceso se cae al salir de la función | Apartado 3.2: estado y vida útil por operación |
| ReadFile vuelve con FALSE, o una finalización síncrona provoca un tratamiento duplicado | Apartado 3.3: las tres ramas del resultado de emitir |
| Quiere elegir cómo recibir la finalización, o no llega la devolución de llamada | Capítulo 4: comparación de las vías de notificación, apartado 4.3: cómo esperar un APC |
| Lo hizo asíncrono y aun así la llamada espera | Capítulo 5: condiciones de finalización síncrona y capacidad de respuesta |
| La cancelación no surte efecto, o el proceso se cae después de cancelar | Capítulo 6: limpiar solo después de confirmar la finalización |
| Usa ReadAsync y aun así aumentan los hilos | Capítulo 7: combinación de handles y API en .NET |
En el diagrama, una línea continua marca una relación que siempre se cumple y una línea discontinua una relación condicional (las condiciones están en la explicación de cada relación en la página de detalle). La lista completa de relaciones (36 en total, con evidencia y grado de certeza) y las definiciones de los conceptos principales están reunidas en la página de detalle del mapa de conocimiento (en japonés). Datos: JSON-LD / Turtle
2. E/S síncrona: el hilo que espera la finalización duerme sin consumir CPU
2.1. Quien espera es el administrador de E/S
Un handle abierto sin FILE_FLAG_OVERLAPPED está en modo síncrono. ReadFile no vuelve hasta que la E/S se completa.1
Cuando el controlador deja la solicitud en espera (pendiente) porque aguarda la respuesta del hardware, el administrador de E/S espera la finalización y después devuelve el control a la aplicación. El hilo de la aplicación espera dentro del núcleo durante ese tiempo.
sequenceDiagram
participant App as Hilo de la aplicación
participant IOM as Administrador de E/S
participant DRV as Controlador (pila)
App->>IOM: ReadFile(handle síncrono)
IOM->>DRV: Emitir el IRP
DRV-->>IOM: STATUS_PENDING (esperando respuesta)
Note over App,IOM: El hilo pasa a estado de espera dentro del núcleo<br/>y duerme sin consumir CPU
DRV->>IOM: Finalización (IoCompleteRequest)
IOM-->>App: Devuelve el resultado y despierta<br/>ReadFile vuelve con TRUE/FALSE
Figura 1: E/S síncrona en la que la solicitud queda pendiente. ReadFile vuelve después de esperar la finalización
Ahora bien, que sea E/S síncrona no significa que el hilo duerma siempre. Si la solicitud se puede completar en el acto, por ejemplo por un acierto de caché, el resultado vuelve sin espera (la vía de «finalización inmediata» de la figura 5 de la parte 1). Lo que se garantiza es que «no vuelve antes de completarse».
2.2. No consumir CPU y poder hacer otro trabajo son cosas distintas
Un hilo en estado de espera sale del conjunto de hilos ejecutables del programador, así que no consume CPU. Por qué conviene dejarlo esperar en lugar de seguir sondeando uno mismo también se explica en «Por qué priorizar la espera por eventos sobre Sleep(1) en Windows».
Por otro lado, un hilo en espera no puede hacer otro trabajo. Si es el hilo de la interfaz de usuario, la pantalla se congela; si un servidor prepara un hilo por cada conexión, con unos cientos de conexiones aumentan los hilos. El punto débil de la E/S síncrona no es el uso de CPU, sino que ese hilo no se puede usar hasta que se complete.
En modo síncrono el núcleo también gestiona el puntero de archivo (la posición actual). Por eso las llamadas consecutivas a ReadFile leen «a partir de donde quedó la anterior». La posición pertenece al objeto de archivo que hay detrás del handle, así que los handles duplicados con DuplicateHandle comparten la posición (apartado 3.3 de la parte 1).
Además existe CancelSynchronousIo, que solicita la cancelación de una E/S síncrona en ejecución en otro hilo. El capítulo 6 resume cuándo usarla frente a las API pensadas para E/S asíncrona.5
3. Preparar y emitir E/S asíncrona: separar el modo, el estado y el valor de retorno
3.1. El modo asíncrono se decide al abrir el archivo
Si se pasa FILE_FLAG_OVERLAPPED a CreateFile, el objeto de archivo detrás del handle pasa a modo asíncrono. El modo no se alterna en cada llamada. Se puede abrir el mismo archivo con dos handles, uno síncrono y otro asíncrono; en ese caso también hay dos objetos de archivo.1
En modo asíncrono el sistema no gestiona el puntero de archivo. Como se pueden emitir varias operaciones a la vez, en un archivo en disco la posición de lectura o escritura se indica cada vez con OVERLAPPED.Offset / OffsetHigh. En dispositivos sin posición de búsqueda, como un puerto serie o una canalización con nombre, esa posición no se usa y se deja en 0. Aunque no se indique posición, hace falta un OVERLAPPED exclusivo de la operación.6
Al contrario, pasar un OVERLAPPED a un handle en modo síncrono no lo vuelve asíncrono. Lee desde la posición de Offset, pero el comportamiento de bloquear hasta completarse no cambia. Lo que hay que comprobar no es si se pasó la estructura, sino en qué modo se abrió el handle.6
3.2. Correspondencia de un OVERLAPPED y un búfer con una sola operación
OVERLAPPED es la estructura que identifica una operación en curso y transporta su posición, su estado y su resultado. Pensarla como «el comprobante de una operación» aclara la división del trabajo con el handle.3
| Miembro | Función |
|---|---|
Offset / OffsetHigh |
Posición del archivo que esta operación lee o escribe (se indica al emitir; no se usa en dispositivos sin posición) |
hEvent |
Evento que se señaliza al completarse (opcional; se recomienda uno de reinicio manual) |
Internal |
Estado de la operación. Antes de completarse contiene el equivalente de STATUS_PENDING (uso del sistema) |
InternalHigh |
Número de bytes transferidos al completarse (uso del sistema) |
flowchart LR
subgraph H["Handle (objeto de archivo) = modo"]
M1["Modo síncrono<br/>el núcleo gestiona la posición actual<br/>ReadFile no vuelve hasta completarse"]
M2["Modo asíncrono (FILE_FLAG_OVERLAPPED)<br/>no se gestiona la posición actual<br/>la emisión y la finalización se separan"]
end
subgraph O["Estructura OVERLAPPED = comprobante de una operación"]
F1["Offset: de dónde leer"]
F2["hEvent: cómo enterarse de la finalización"]
F3["Internal/InternalHigh:<br/>estado y resultado (los escribe el sistema)"]
end
C["Se decide una sola vez, en CreateFile"] --> H
R["Se prepara uno en cada emisión de ReadFile/WriteFile"] --> O
Figura 2: El handle guarda el modo; OVERLAPPED, la posición y el estado de cada operación
Aquí hay que respetar dos cosas: el número y la vida útil. Si se emiten tres E/S a la vez, se preparan tres OVERLAPPED. Compartir la misma estructura entre varias operaciones no completadas lleva a resultados impredecibles o a la corrupción de datos.2
Además, hasta que se complete hay que mantener válidos la estructura y el búfer de datos, sin modificarlos, reutilizarlos ni liberarlos. El núcleo sigue usando esa zona. Si se emite con un OVERLAPPED de variable local y se sale de la función mientras la operación sigue en curso, se está dejando usar una zona de pila cuya vida útil ya terminó.32
Cuando se reutiliza después de confirmar la finalización, se reinicializa para que no quede el estado de la operación anterior. Si se elige la vía de eventos, se usa un evento de reinicio manual en hEvent. La relación con la forma de esperar se explica en el apartado 4.2.3
3.3. Dividir el valor de retorno de ReadFile en tres y decidir dónde tratarlo
El resultado de emitir ReadFile contra un handle asíncrono se juzga con la combinación del valor de retorno y GetLastError(). Lo importante es no tratar todo FALSE como un fallo.6
Valor de retorno de ReadFile |
GetLastError() |
Significado | Qué hace el llamador |
|---|---|---|---|
TRUE |
(no se mira) | Se completó en el acto (finalización síncrona) | Por defecto también llega una notificación de finalización. Deje el tratamiento del resultado al lado de la notificación |
FALSE |
ERROR_IO_PENDING (997) |
Aceptada. En curso | No hacer nada. Esperar la notificación de finalización sin tocar el OVERLAPPED ni el búfer |
FALSE |
Cualquier otro | Falló la propia emisión | La notificación de finalización no llega. Trate el error en el acto y limpie el OVERLAPPED y el búfer |
flowchart TB
A["ReadFile(handle asíncrono, con OVERLAPPED)"]
Q{"¿Cuál es el valor de retorno?"}
T["TRUE<br/>se completó en el acto (finalización síncrona)<br/>por defecto también llega una notificación"]
P["FALSE + ERROR_IO_PENDING<br/>aceptada. La finalización se notifica después"]
E["FALSE + otro error<br/>falló la propia emisión"]
W["Esperar la notificación de finalización<br/>(las 4 vías del capítulo 4)"]
A --> Q
Q --> T
Q --> P
Q --> E
P --> W
Figura 3: Tratar las tres ramas: finalización síncrona, aceptada y en curso, y fallo de emisión
La función siguiente solo hace este juicio y lo devuelve al llamador. Se da por hecho que en otro sitio se preparan el handle, la estructura, el búfer y el evento exclusivos de la operación, y el código que recibe la finalización.
// C++ / Win32
// hFile : handle abierto con FILE_FLAG_OVERLAPPED
// ov : OVERLAPPED reservado en exclusiva para esta operación (Offset y hEvent ya están configurados)
// buf/len: búfer exclusivo de esta operación. No se libera hasta recibir la notificación de finalización
DWORD IssueRead(HANDLE hFile, OVERLAPPED* ov, BYTE* buf, DWORD len)
{
// En una emisión asíncrona se pasa NULL en lpNumberOfBytesRead y
// el número de bytes transferidos se obtiene después con GetOverlappedResult
if (ReadFile(hFile, buf, len, nullptr, ov))
{
// (1) Finalización síncrona. Por defecto también llega la notificación, así que aquí no se trata el resultado
return ERROR_SUCCESS;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// (2) Aceptada. Esperar la notificación de finalización sin tocar ov ni buf
return ERROR_IO_PENDING;
}
// (3) Fallo de la propia emisión. No llega notificación, así que el llamador limpia aquí
return err;
}
ERROR_IO_PENDING es el resultado «aceptada y aún no completada». No se debe limpiar como un error habitual. La finalización síncrona que vuelve con TRUE también es una vía normal, así que hay que tratarla siempre. Las razones de una finalización síncrona se explican en el capítulo 5.
El resultado se trata una sola vez. Por defecto, incluso en una operación que se completó de forma síncrona, si el handle está asociado a un IOCP se encola un paquete de finalización, y si se usa la vía de eventos el evento se señaliza. Si se trata el resultado tanto justo después de TRUE como al recibir la notificación, se procesa dos veces la misma operación y se corre el riesgo de liberar dos veces la estructura. La forma básica segura es unificar las vías TRUE y ERROR_IO_PENDING en el tratamiento del resultado del lado de la notificación.1
En cambio, en la vía en la que falló la propia emisión, el emisor hace el tratamiento del error y la limpieza. Si se pasa a esperar cuando no va a llegar ninguna notificación, se espera para siempre.
También existe una optimización que omite la notificación al IOCP en una finalización síncrona, pero es un diseño distinto del comportamiento predeterminado. El ámbito de aplicación de FILE_SKIP_COMPLETION_PORT_ON_SUCCESS se explica por separado en el apartado 5.3.7
4. Elegir la notificación de finalización: se decide por el número de E/S y por el hilo que las trata
Una vez separadas la emisión y la finalización, hace falta «cómo recibir la finalización». Empiece por comparar las cuatro vías según el número de E/S que se tratan a la vez y el hilo que ejecuta el tratamiento de finalización.1
| Vía | Hilo que ejecuta el tratamiento de finalización | Número de E/S que se pueden lanzar a la vez | Dónde encaja |
|---|---|---|---|
| (1) Señal del handle | Cualquier hilo que espere | En la práctica, 1. Si se lanzan varias no se distingue cuál se completó | Casi ninguna (4.1) |
(2) Evento + GetOverlappedResult |
Cualquier hilo que espere | Hace falta un evento por operación. Si se esperan juntas con WaitForMultipleObjects el tope es 64 |
Unas pocas E/S concurrentes. Comunicación con dispositivos (4.2) |
(3) APC (ReadFileEx) |
El hilo que emitió, y solo mientras está en espera alertable | No hay límite de número, pero todo el tratamiento de finalización corre en serie en ese único hilo | Comunicación que se quiere cerrar en un solo hilo (4.3) |
| (4) Puerto de finalización de E/S | El conjunto de hilos trabajadores ligados al puerto | Muchas E/S las pueden atender pocos hilos | Servidores, grupo de subprocesos (4.4) |
flowchart TB
DONE["La E/S se completa en el núcleo<br/>(IoCompleteRequest → un APC confirma el resultado)"]
N1["(1) El handle de archivo pasa a estado señalizado<br/>recepción: WaitForSingleObject(handle)"]
N2["(2) El hEvent de OVERLAPPED pasa a estado señalizado<br/>recepción: WaitForSingleObject + GetOverlappedResult"]
N3["(3) La rutina de finalización se encola en la cola de APC del hilo emisor<br/>recepción: se ejecuta durante una espera alertable como SleepEx"]
N4["(4) Un paquete de finalización entra en el puerto de finalización de E/S<br/>recepción: GetQueuedCompletionStatus (parte 3)"]
DONE --> N1
DONE --> N2
DONE --> N3
DONE --> N4
Figura 4: Las cuatro vías de notificación de finalización. La forma de recibirla cambia según cómo se emitió
4.1. Señal del handle: no se pueden distinguir varias operaciones
Si se emite sin indicar hEvent, al completarse el propio handle de archivo pasa a estado señalizado. Sin embargo, si hay varias operaciones en curso sobre el mismo handle, no se distingue cuál se completó.1
Salvo el caso especial de «no emitir nunca más de una E/S asíncrona a la vez», lo seguro es no usarla. Aunque parezca cómoda, no llega a ser un mecanismo para gestionar el resultado de cada operación.
4.2. Eventos y GetOverlappedResult: la forma básica para unas pocas E/S concurrentes
Se configura un evento de reinicio manual en el OVERLAPPED.hEvent de cada operación y se emite. Tras esperar con WaitForSingleObject, se obtienen el éxito o el fallo y el número de bytes transferidos con GetOverlappedResult. Para esperar varios eventos juntos se usa WaitForMultipleObjects, pero el máximo simultáneo es 64.18
Si bWait de GetOverlappedResult se pone en TRUE, también se puede esperar hasta la finalización y entonces tomar el resultado. Si aquí se usa un evento de reinicio automático, GetOverlappedResult puede seguir esperando después de que otra espera haya consumido la señal. Se usa reinicio manual precisamente para evitar este problema de sincronización.83
Para tratar con solidez unas pocas E/S concurrentes, es una vía clara. También se usa en el tratamiento de «leer mientras se escribe» de un puerto serie. Un ejemplo práctico está en «Puntos débiles de las aplicaciones de comunicación serie».
4.3. APC: mantener el hilo emisor en espera alertable hasta la finalización
ReadFileEx / WriteFileEx son la vía en la que se indica una rutina de finalización (devolución de llamada). Cuando la E/S se completa, la rutina se encola en la cola de APC del hilo que la emitió. Se ejecuta cuando ese hilo entra en espera alertable con SleepEx, WaitForSingleObjectEx u otra llamada similar.91011
Como el tratamiento de finalización corre en serie en el mismo hilo, en un procesamiento que se cierra en un solo hilo se pueden evitar los bloqueos. Por otro lado, si el hilo emisor no entra en espera alertable, la rutina de finalización no se ejecuta. Combinarla con el bucle de mensajes de la interfaz de usuario exige MsgWaitForMultipleObjectsEx, y el diseño de la espera se complica. En uso general se elige con más frecuencia un evento o un IOCP.
Lo más fácil de pasar por alto con un APC es si se pudo emitir, si la forma de esperar es correcta y si se completó la propia operación. El código siguiente es un extracto que contrasta una espera mala con una buena; no es un ejemplo de ejecutar las dos seguidas. Se da por hecho que en otro sitio se preparan el handle y el búfer, y OnReadCompleted, que actualiza el indicador de finalización de cada operación.
// C++ / Win32. hFile es un handle abierto con FILE_FLAG_OVERLAPPED,
// y ov y buf se mantienen vivos hasta la finalización (apartado 3.2)
// Ejemplo malo: la rutina de finalización nunca se llama
ReadFileEx(hFile, buf, len, ov, OnReadCompleted);
Sleep(1000); // espera que no es alertable. El APC no se entrega
// Ejemplo bueno: seguir en espera alertable hasta que termine esta E/S
//
// La rutina de finalización pone este indicador (por ejemplo, en la estructura que aloja ov)
volatile bool completed = false;
// Hay que comprobar siempre si se pudo emitir. Si devuelve 0, no se encoló ninguna rutina de finalización
if (!ReadFileEx(hFile, buf, len, ov, OnReadCompleted))
{
const DWORD err = GetLastError(); // tomarlo enseguida. Las API posteriores lo sobrescriben
ReportError(err); // desconexión del dispositivo, handle no válido, etc.
return; // * no se debe entrar en el bucle de espera de abajo
}
while (!completed)
{
DWORD r = SleepEx(1000, TRUE); // el TRUE del segundo argumento es alertable
if (r == WAIT_IO_COMPLETION)
{
// Se ejecutó algún APC. No tiene por qué ser la propia E/S,
// así que se decide mirando completed y, si no es esa, se vuelve a esperar
continue;
}
// Volvió por tiempo de espera. La E/S sigue emitida, así que
// si se va a cortar, se cancela con CancelIoEx y se espera a que se entregue la finalización
CancelIoEx(hFile, ov);
}
Si la emisión falla, no se entra a esperar. Si ReadFileEx devuelve 0 por desconexión del dispositivo, un handle no válido u otra causa, no se ha encolado ninguna rutina de finalización. Se toma GetLastError() enseguida, se trata el error y se sale. Si se pasa esto por alto, completed no se pone nunca a verdadero y se repiten SleepEx y CancelIoEx contra una E/S que no existe.9
El tiempo de espera de la espera no se toma por el fin de la E/S. Cuando SleepEx agota el tiempo sale de la espera alertable, pero la E/S ya emitida puede seguir ahí. No se debe salir del ámbito y dejar caducar ov o buf. O se sigue esperando hasta la finalización, o, si se corta, se solicita la cancelación y se espera a que se entregue esa finalización. La regla de vida útil del apartado 3.2 sigue igual después de un tiempo de espera.
No se concluye solo con WAIT_IO_COMPLETION que terminó la propia E/S. Ese valor de retorno significa que se ejecutó al menos un APC. Si en el mismo hilo hay otra E/S o un APC de QueueUserAPC, también vuelve por eso. Se juzga con el indicador que actualiza la propia rutina de finalización y, si aún no está, se vuelve a esperar.10
Cuando «no llega el APC», además del éxito o el fallo de la emisión se comprueba la función de espera. Que sea SleepEx(..., TRUE) y no Sleep, WaitForSingleObjectEx(..., TRUE) y no WaitForSingleObject. La Ex del final y el TRUE del argumento alertable son los puntos a comprobar.10
4.4. IOCP: atender muchas E/S con pocos trabajadores
Con un puerto de finalización de E/S (IOCP) se asocia el handle a un puerto. Los paquetes de finalización entran en la cola del puerto y los hilos trabajadores los extraen con GetQueuedCompletionStatus. Es el mecanismo para tratar muchas E/S concurrentes con pocos hilos.12
También es la vía que sostiene la E/S asíncrona de .NET. Cómo se combinan la cola de notificaciones de finalización y el control del número de hilos que se ejecutan en paralelo se trata con detalle en la parte 3.
5. La excepción de la finalización síncrona: «asíncrono» y «no se le hace esperar» no son lo mismo
5.1. Condiciones típicas en las que se completa dentro de la llamada
Aunque se emita correctamente en modo asíncrono, la E/S puede completarse dentro de la llamada. Finalización síncrona significa que la E/S terminó antes de que la función volviera; no es una promesa de que vuelva en poco tiempo. Hay que separar el caso que termina rápido por un acierto de caché del caso en que se le hace esperar dentro de la llamada.2
flowchart TB
A["Emitir ReadFile/WriteFile contra un handle asíncrono"]
Q{"¿Cae en una condición de finalización síncrona?"}
C1["Solicitud que se puede satisfacer de inmediato<br/>(los datos ya están en la caché, etc.)"]
C2["Archivo comprimido con NTFS<br/>(un archivo comprimido no se vuelve asíncrono)"]
C3["Archivo cifrado con NTFS (EFS)"]
C4["Escritura que alarga la longitud del archivo"]
T["Vuelve TRUE de inmediato<br/>= se ejecutó hasta completarse dentro de la llamada"]
P["Vuelve con ERROR_IO_PENDING<br/>= realmente en curso de forma asíncrona"]
A --> Q
Q --> C1
Q --> C2
Q --> C3
Q --> C4
C1 --> T
C2 --> T
C3 --> T
C4 --> T
Q -->|"ninguna de ellas"| P
Figura 5: Condiciones principales en las que una E/S emitida de forma asíncrona se completa de forma síncrona. Distinguirlo del tiempo que tarda la llamada en volver
El documento de resolución de problemas de Microsoft cita las razones siguientes.2
| Condición | Por qué se trata de forma síncrona y qué implica en el código |
|---|---|
| Solicitud que se puede satisfacer de inmediato o acierto de caché | Si los datos están en memoria, el controlador puede completar en el acto. Que termine rápido está bien, pero el código que presupone que siempre vuelve ERROR_IO_PENDING se rompe |
| Lectura con caché activa en la que falta la página necesaria | La caché de Windows está implementada con asignación de archivos. Como no hay un mecanismo asíncrono de error de página, a veces se trata de forma síncrona |
| Archivo comprimido con NTFS o cifrado con EFS | El controlador del sistema de archivos convierte el acceso a síncrono |
| Escritura que alarga el archivo | Una escritura que cambia la longitud se vuelve síncrona |
Lo importante es que un tratamiento síncrono puede ocurrir no solo con un acierto de caché, sino también cuando los datos no están en la caché. El mecanismo de la caché en sí se trata en la parte 4 de la serie.
5.2. Diseñar por separado las ramas del resultado de emitir y la capacidad de respuesta de la interfaz
Lo primero que hace falta es tratar las tres ramas del apartado 3.3. También TRUE se contempla como un resultado normal y, por defecto, el tratamiento del resultado se unifica en el lado de la notificación.
Ahora bien, escribir bien las ramas no garantiza la capacidad de respuesta. Como no se puede decir «es E/S asíncrona, así que la interfaz no se congela», hace falta un diseño que separe la propia emisión de la E/S de cualquier hilo que no se pueda detener, y la deje a un hilo dedicado o al grupo de subprocesos. La práctica relacionada se explica también en «Guía práctica para lograr el máximo de soft real-time posible en un Windows convencional».
5.3. Omitir la notificación en una finalización síncrona es una optimización solo del IOCP
En E/S de alta frecuencia se puede optimizar omitiendo la notificación en una finalización síncrona. Si se activa FILE_SKIP_COMPLETION_PORT_ON_SUCCESS con SetFileCompletionNotificationModes, no se encola en el IOCP el paquete de finalización de una E/S que tuvo éxito de inmediato. Es la opción para cuando se pasa a un diseño que trata el resultado en el acto, no en el lado de la notificación.7
Lo que se omite es solo el paquete hacia el IOCP: la señal de OVERLAPPED.hEvent no se suprime. No aplique la misma optimización a la vía de eventos. Mezclar la vía de notificación predeterminada con la vía ya optimizada lleva al tratamiento duplicado del apartado 3.3 o a esperar una notificación que no llega. La combinación con IOCP se trata en la parte 3.
6. Cancelación y cierre: solicitar, confirmar la finalización, cerrar
6.1. Elegir la API según lo que se quiere anular
Las API de cancelación se eligen según la operación y el hilo que la emitió.4135
| API | Destino y forma de indicarlo |
|---|---|
CancelIoEx |
Solicita la cancelación de la E/S no completada de un handle indicado, sin importar qué hilo la emitió. Si el segundo argumento es un OVERLAPPED, esa operación; si es NULL, todas las operaciones de ese handle |
CancelIo |
Solo las operaciones que emitió el propio hilo que llama |
CancelSynchronousIo |
La E/S síncrona en ejecución en otro hilo indicado |
CancelIoEx se introdujo en Vista. En E/S asíncrona no hay hoy razón para usar a propósito el antiguo CancelIo, limitado al hilo emisor; la base es CancelIoEx.
6.2. El éxito de CancelIoEx no significa que la E/S haya terminado
CancelIoEx es una API que solicita la cancelación de IRP no completados, no una API que espera la finalización de la operación. Aunque tenga éxito, solo se ha solicitado la anulación. Una operación que ya estaba a punto de completarse puede terminar con normalidad porque la cancelación no llegó a tiempo.14
sequenceDiagram
participant App as Aplicación
participant IOM as Administrador de E/S
participant DRV as Controlador
App->>IOM: CancelIoEx(handle, OVERLAPPED)
Note over IOM: Solicitar la cancelación del IRP<br/>no completado correspondiente (marcarlo)
IOM->>DRV: Llamada a la rutina de cancelación
Note over DRV: Si se puede anular, se interrumpe<br/>si está a punto de terminar, puede completar con normalidad
DRV->>IOM: IoCompleteRequest<br/>(STATUS_CANCELLED)
IOM-->>App: Llega la notificación de finalización<br/>GetOverlappedResult indica ERROR_OPERATION_ABORTED
Note over App: Tras observar esta notificación<br/>se liberan OVERLAPPED y el búfer
Figura 6: Una operación anulada también se notifica como finalización. La limpieza se hace después de esa confirmación
Una operación que realmente se anuló vuelve en la notificación de finalización como ERROR_OPERATION_ABORTED. Tanto si terminó con normalidad como si se anuló, no se liberan la estructura ni el búfer hasta recibir la notificación. Liberarlos antes hace que el núcleo pierda una zona que aún usa, y eso lleva a la corrupción de memoria. En una infracción de acceso después de cancelar, lo primero es comprobar esta vida útil.414
6.3. Recuperar las operaciones ya emitidas antes de cerrar el handle
La secuencia básica de cierre es solicitar la cancelación → observar la finalización → cerrar el handle.
Como se vio en la parte 1, al cerrar el último handle el tratamiento de limpieza anula los IRP no completados. Sin embargo, cerrar solo el handle dejando E/S ya emitidas suele romper la gestión de las notificaciones de finalización y de la vida útil de los búferes. No se delega la limpieza al cierre: primero se cierran las operaciones no completadas.
Lo mismo vale cuando se quiere cortar el tratamiento por tiempo de espera. El sistema operativo no decide por la aplicación hasta dónde cortar, así que se diseñan juntas la cancelación posterior al tiempo de espera y el procedimiento para recibir la finalización. En la vía APC, como en el apartado 4.3, se sigue en espera alertable hasta que se entregue la finalización.
7. Correspondencia con .NET: no basta con ReadAsync; hay que mirar dónde se abre el archivo
7.1. Hacer coincidir el modo del handle y la API que se llama
El useAsync de FileStream, o FileOptions.Asynchronous, corresponde a FILE_FLAG_OVERLAPPED de Win32. Igual que en la tabla de correspondencias de la parte 1, también en .NET importa el modo al abrir el archivo.1516
flowchart TB
A["await fs.ReadAsync(...)"]
Q{"¿El handle está en modo asíncrono<br/>(FileOptions.Asynchronous)?"}
Y["E/S asíncrona real<br/>se emite el equivalente de OVERLAPPED y<br/>la finalización llega al grupo de subprocesos por IOCP (parte 3)"]
N["Asincronía aparente<br/>un hilo del grupo de subprocesos se hace cargo<br/>y espera en un Read síncrono"]
A --> Q
Q -->|sí| Y
Q -->|no| N
Figura 7: Aunque sea el mismo ReadAsync, el modo del handle cambia la vía de tratamiento en el sistema operativo
| Combinación de handle y API | Qué ocurre por dentro |
|---|---|
Modo asíncrono + ReadAsync / WriteAsync |
La combinación que usa la E/S asíncrona del sistema operativo |
Modo síncrono + ReadAsync / WriteAsync |
Un hilo del grupo de subprocesos se hace cargo de la lectura o escritura síncrona: «asincronía aparente» |
Modo asíncrono + Read / Write síncronos |
Se produce el coste de esperar internamente la finalización |
También con la «asincronía aparente» el hilo que llama no espera, pero por detrás espera otro hilo. Si son pocos casos el daño real es pequeño, pero en un servidor o en un tratamiento de alta frecuencia se convierte en causa de agotamiento del grupo de subprocesos y de pérdida de escalabilidad. El principio es hacer coincidir el modo y la API.1615
7.2. Comparar tres formas de crear un FileStream
En (A) y (B) siguientes, la forma de llamar a ReadAsync es la misma. Lo único que cambia es useAsync al abrir el archivo. (C) es el ejemplo, a partir de .NET 6, que hace explícitos el handle y la posición.
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) Asincronía aparente. Si se omite useAsync o se pone en false, el handle se abre en modo síncrono
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: false))
{
// El llamador no se bloquea, pero por detrás un hilo del grupo de subprocesos se hace cargo de un Read síncrono y espera
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (B) E/S asíncrona real. useAsync: true enlaza de forma directa con FILE_FLAG_OVERLAPPED
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true))
{
// La finalización llega al grupo de subprocesos por IOCP (parte 3)
await fs.ReadAsync(buffer, 0, buffer.Length);
}
// (C) A partir de .NET 6. Forma directa que hace explícitos el modo y el desplazamiento
using (SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read,
options: FileOptions.Asynchronous))
{
int read = await RandomAccess.ReadAsync(handle, buffer, fileOffset: 0);
}
Al revisar código existente, no se busca solo el lado que llama a ReadAsync / WriteAsync, sino el lugar donde se crea el FileStream. Las sobrecargas cortas como File.OpenRead o new FileStream(path, FileMode.Open) abren en modo síncrono. Si se crea un FileStream a partir de un SafeFileHandle, el argumento isAsync también se hace coincidir con el modo real del handle.
7.3. Con RandomAccess se hacen explícitos el handle y el desplazamiento
En .NET 6 se reescribió por completo la implementación interna de FileStream y se añadieron File.OpenHandle y RandomAccess. Son API que tratan directamente un SafeFileHandle y pasan en cada llamada la posición de lectura o escritura.16
La forma de (C), que hace explícitos el modo y fileOffset, corresponde a la división del trabajo de este artículo: handle asíncrono y OVERLAPPED.Offset por operación.
7.4. También con CancellationToken la cancelación se queda en una solicitud
En un handle en modo asíncrono, la cancelación de E/S de archivo mediante CancellationToken termina internamente en CancelIoEx. Detrás de un ReadAsync al que se le pasó el token y que termina con OperationCanceledException está funcionando el mecanismo del capítulo 6. Tampoco aquí se garantiza una interrupción inmediata.
En la «asincronía aparente» de un handle en modo síncrono no hay una operación superpuesta a la que apuntar la cancelación, así que esta vía no está disponible. Los runtimes recientes de .NET incluyen además un mecanismo que intenta anular, con CancelSynchronousIo, una llamada en ejecución síncrona, pero el comportamiento depende de la versión del runtime y del tipo de operación, y no se garantiza una interrupción segura. Si el diseño presupone la cancelación, lo propio es igualar el modo del handle y usar la E/S asíncrona del sistema operativo.
La práctica de la capa de async/await —por ejemplo ConfigureAwait y la relación con el hilo de la interfaz de usuario— está en «Tabla práctica de decisión de C# async/await - Task.Run y ConfigureAwait» y «WPF/WinForms: async y el hilo de UI, resumidos en una hoja». Este artículo explica cómo el sistema operativo avanza las lecturas y escrituras por debajo de esa capa.
8. Resumen: comprobar de la emisión a la limpieza como un solo recorrido
Al revisar E/S asíncrona, se sigue el código en este orden.
- En el lugar que abre, confirmar el modo síncrono o asíncrono. Si es un archivo en disco, indicar la posición en cada operación asíncrona.
- En el lugar que emite, confirmar que hay un
OVERLAPPEDy un búfer exclusivos de la operación, y que se tratan las tres ramas del apartado 3.3. - En el lugar que recibe la finalización, confirmar que la forma de esperar encaja con la vía (evento, APC, IOCP) y que no se trata dos veces el mismo resultado.
- En el lugar que cierra, confirmar que no se libera nada solo con un tiempo de espera o una solicitud de cancelación.
La E/S síncrona y la asíncrona no son tuberías distintas. La diferencia es si se vuelve después de esperar la finalización o se usa una vía que vuelve antes. Como también en modo asíncrono ocurre la finalización síncrona, se separan las ramas del resultado de emitir y el diseño de la capacidad de respuesta.12
El modo pertenece al handle, el estado a cada operación, y la limpieza solo después de confirmar la finalización. Esta división del trabajo es común tanto si se trata directamente el OVERLAPPED de Win32 como si se usa FileOptions.Asynchronous de .NET. También una operación cancelada sigue bajo gestión hasta que se reciba su finalización.3415
La continuación es la parte 3, «Los entresijos de E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await». Trata por qué el IOCP del apartado 4.4 unifica la cola de notificaciones de finalización y el control del número de hilos en ejecución, y en qué hilo corre la continuación de un await.
Artículos relacionados
- Las profundidades de la E/S de Windows (parte 1) — Toda lectura y escritura se convierte en un IRP: panorama general del sistema de E/S
- Tabla práctica de decisión de C# async/await - Task.Run y ConfigureAwait
- WPF/WinForms: async y el hilo de UI, resumidos en una hoja
- Puntos débiles de las aplicaciones de comunicación serie - hasta el diseño de la reconexión y los registros
- Por qué priorizar la espera por eventos sobre Sleep(1) en Windows
- Guía práctica para lograr el máximo de soft real-time posible en un Windows convencional
- El malentendido de que TCP permite recibir por cada unidad enviada con Send ── diseño de recepción para tratarlo como flujo de bytes
Áreas de consultoría relacionadas
En KomuraSoft LLC nos ocupamos del diseño de aplicaciones empresariales Windows y de comunicación con dispositivos que usan E/S asíncrona, y de la investigación de causas de fallos como «se congela», «se cae al cancelar» o «se agota el grupo de subprocesos».
- Desarrollo de aplicaciones para Windows
- Investigación de errores y análisis de causa raíz
- Desarrollo de aplicaciones de tiempo real flexible para Windows
- Contacto
Referencias
-
Microsoft Learn, Synchronous and asynchronous I/O. Sobre que en la E/S síncrona la función se bloquea hasta que la E/S se completa, mientras que en la E/S asíncrona la función que emitió la solicitud vuelve de inmediato y el hilo puede seguir con otro trabajo; que la E/S asíncrona requiere abrir el handle indicando FILE_FLAG_OVERLAPPED; que como métodos de notificación de finalización existen la señal del handle de archivo, la señal del evento indicado en la estructura OVERLAPPED, la rutina de finalización (APC) ejecutada en una espera alertable, y el puerto de finalización de E/S; y que cuando hay varias operaciones emitidas al mismo tiempo, la señal del handle de archivo no permite distinguir cuál se completó. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Sobre las razones por las que una E/S codificada como asíncrona termina completándose de forma síncrona: un archivo comprimido con NTFS (el controlador del sistema de archivos no accede de forma asíncrona a los archivos comprimidos, así que todas las operaciones se vuelven síncronas), un archivo cifrado con NTFS, una escritura que alarga la longitud del archivo, y que cuando la solicitud se puede satisfacer de inmediato (por ejemplo, cuando los datos ya están en la caché de memoria) el controlador completa la operación en el acto y devuelve TRUE; que la caché de Windows está implementada mediante asignación de archivos y no dispone de un mecanismo asíncrono de error de página cuando la página no está presente; y además, que si se emiten tres E/S hacen falta tres estructuras OVERLAPPED, que reutilizarlas lleva a resultados impredecibles o a la corrupción de datos, y que no se debe leer ni escribir en el búfer de datos correspondiente hasta que la operación se complete. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, OVERLAPPED structure. Sobre que la estructura OVERLAPPED conserva la información para la entrada y salida asíncrona; que Offset/OffsetHigh contienen la posición del archivo, hEvent el evento que se señaliza al completarse, e Internal/InternalHigh el código de estado de la operación y el número de bytes transferidos; que mientras la operación se ejecuta no se debe modificar la estructura, la cual debe mantenerse válida; y las precauciones al usar el evento. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, CancelIoEx function. Sobre que CancelIoEx marca para cancelación la E/S no completada de un handle indicado, sin importar qué hilo la emitió; que si se especifica lpOverlapped solo se apunta a esa operación, y con NULL a todas las E/S no completadas; que una operación cancelada se completa con ERROR_OPERATION_ABORTED; y que no se garantiza la cancelación de todas las operaciones, por lo que hay que esperar a que termine el tratamiento de finalización. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, CancelSynchronousIo function. Sobre que CancelSynchronousIo marca para cancelación la operación de E/S síncrona que un hilo indicado está ejecutando, y que la operación cancelada se devuelve como fallida con ERROR_OPERATION_ABORTED. ↩ ↩2
-
Microsoft Learn, ReadFile function. Sobre que en un handle abierto con FILE_FLAG_OVERLAPPED, lpOverlapped es obligatorio y la posición de inicio de lectura se indica mediante Offset/OffsetHigh de la estructura OVERLAPPED; que cuando se procesa de forma asíncrona se devuelve FALSE y ERROR_IO_PENDING; que el sistema no mantiene el puntero de archivo de un handle asíncrono; y que si se pasa un OVERLAPPED a un handle abierto sin FILE_FLAG_OVERLAPPED, la lectura parte del desplazamiento indicado pero ReadFile no vuelve hasta que la lectura se completa. ↩ ↩2 ↩3
-
Microsoft Learn, SetFileCompletionNotificationModes function. Sobre que FILE_SKIP_COMPLETION_PORT_ON_SUCCESS permite elegir que, cuando la E/S tiene éxito de inmediato, no se encole el paquete de finalización en el puerto de finalización de E/S, y que FILE_SKIP_SET_EVENT_ON_HANDLE permite omitir la señalización del evento del handle de archivo. ↩ ↩2
-
Microsoft Learn, GetOverlappedResult function. Sobre que GetOverlappedResult obtiene el resultado de una operación asíncrona (éxito o fallo y bytes transferidos); que pasar TRUE en bWait hace que espere hasta la finalización de la operación; y que si se especifica un evento de reinicio automático en el hEvent de OVERLAPPED y otra espera consume la señal, una llamada con bWait=TRUE puede no detectar la finalización y quedarse esperando, por lo que conviene usar un evento de reinicio manual. ↩ ↩2
-
Microsoft Learn, ReadFileEx function. Sobre que ReadFileEx recibe una rutina de finalización (FileIOCompletionRoutine) que se llama cuando la lectura se completa; que la rutina de finalización se ejecuta cuando el hilo que la llamó está en estado de espera alertable; y que se necesita un handle abierto con FILE_FLAG_OVERLAPPED. ↩ ↩2
-
Microsoft Learn, Alertable I/O. Sobre que en la E/S alertable la entrada a la rutina de finalización se encola en la cola de APC del hilo; que el APC se ejecuta cuando el hilo entra en estado alertable mediante SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, etc.; y que el APC siempre se ejecuta en el contexto del hilo que lo emitió. ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous Procedure Calls. Sobre que un APC es una función que se ejecuta de forma asíncrona en el contexto de un hilo determinado, que cada hilo tiene su propia cola de APC, y que un APC en modo usuario solo se ejecuta cuando el hilo está en estado alertable. ↩
-
Microsoft Learn, I/O Completion Ports. Sobre que el puerto de finalización de E/S ofrece un modelo de hilos eficiente para procesar muchas solicitudes de E/S asíncrona en un sistema multiprocesador; que al asociar un handle de archivo al puerto, el paquete de finalización se encola y los hilos trabajadores lo extraen con GetQueuedCompletionStatus; y que el puerto controla el número de hilos que se ejecutan de forma concurrente. ↩
-
Microsoft Learn, CancelIo function. Sobre que CancelIo solo puede cancelar las operaciones de E/S emitidas por el propio hilo que la llama, y que para cancelar también las operaciones emitidas por otros hilos hay que usar CancelIoEx. ↩
-
Microsoft Learn, Canceling pending I/O operations. Sobre el mecanismo de cancelación de E/S no completada; que aunque se solicite la cancelación, la operación puede estar ya encaminada hacia la finalización; que hay que liberar los recursos solo después de confirmar la finalización de la operación cancelada; y la distinción entre usar CancelSynchronousIo para operaciones síncronas y CancelIo/CancelIoEx para operaciones asíncronas. ↩ ↩2
-
Microsoft Learn, Asynchronous file I/O (.NET). Sobre el planteamiento de la E/S de archivo asíncrona en .NET; que para usar E/S asíncrona con FileStream hay que indicar useAsync (FileOptions.Asynchronous) en el constructor para habilitar la E/S asíncrona a nivel de sistema operativo; y la distinción entre usar métodos síncronos o asíncronos. ↩ ↩2 ↩3
-
Microsoft .NET Blog, File IO improvements in .NET 6. Sobre que en .NET 6 se reescribió por completo la implementación interna de FileStream; que la estrategia varía según si el handle está abierto en modo asíncrono o no; que con File.OpenHandle se obtiene directamente un SafeFileHandle y con RandomAccess se pueden hacer lecturas y escrituras (seguras entre hilos) indicando de forma explícita el desplazamiento; y que las llamadas asíncronas sobre un handle que no está en modo asíncrono se descargan al grupo de subprocesos. ↩ ↩2 ↩3
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Las profundidades de la E/S de Windows (parte 4) — El administrador de caché: ¿cuándo llega su WriteFile al disco?
Cuarta entrega de la serie ilustrada sobre el administrador de caché de Windows. Organiza la caché implementada como asignación de archiv...
Las profundidades de la E/S de Windows (parte 1) — Toda lectura y escritura se convierte en un IRP: panorama general del sistema de E/S
Primera entrega de la serie que explica desde la base el sistema de E/S de Windows: el espacio de nombres del Administrador de objetos, l...
Las profundidades de la E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await
Tercera entrega de la serie ilustrada sobre el puerto de finalización de E/S (IOCP). Organiza el diseño que unifica la cola de finalizaci...
Las profundidades del I/O de Windows (6.ª entrega, final) — Cómo funcionan los minifiltros y la investigación de retrasos con Procmon
Cómo los minifiltros vigilan y controlan el I/O de archivos: FltMgr, altitudes, callbacks pre/post, fltmc, operaciones lentas en Procmon,...
Las profundidades de la E/S de Windows (parte 5) — Estructura interna de NTFS: comprender el sistema de archivos a partir de la MFT
Quinta entrega de la serie ilustrada sobre la estructura interna de NTFS. Organiza, desde el punto de vista del desarrollador, la MFT y l...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué cambia al añadir FILE_FLAG_OVERLAPPED?
- El objeto de archivo que hay detrás del handle se abre en «modo asíncrono». Es una propiedad del handle que se decide en el momento de CreateFile, y no se puede alternar entre síncrono y asíncrono en cada llamada. En un handle en modo asíncrono hay que pasar siempre una estructura OVERLAPPED a ReadFile/WriteFile. El sistema no gestiona el puntero de archivo (la posición actual) de este handle, así que en un dispositivo con posición, como un archivo en disco, la posición de lectura o escritura se indica cada vez con Offset de OVERLAPPED (en dispositivos sin noción de posición, como un puerto serie, Offset no se usa). Una operación emitida puede devolver el control antes de completarse; en ese caso ReadFile devuelve FALSE y GetLastError indica ERROR_IO_PENDING. La finalización se recibe con una notificación: un evento, un APC, un puerto de finalización de E/S, etc.
- ¿Por qué, si emito una E/S asíncrona, a veces vuelve ya completada?
- Porque el modo asíncrono significa «no hace falta esperar la finalización», no «nunca se le hará esperar». La documentación de Microsoft cita como razones típicas por las que una E/S emitida de forma asíncrona se completa de forma síncrona: que la solicitud se pueda satisfacer de inmediato (por ejemplo, porque los datos ya están en la caché), un archivo comprimido con NTFS, un archivo cifrado con NTFS (EFS) y una escritura que alarga la longitud del archivo. En esos casos ReadFile/WriteFile devuelve TRUE y el resultado ya está confirmado. Por eso el código que usa E/S asíncrona debe contemplar tanto el caso «vuelve con ERROR_IO_PENDING» como el caso «se completa en el acto», y la capacidad de respuesta tampoco queda garantizada de forma absoluta. Además, por defecto, incluso en una operación que se completó de forma síncrona llega una notificación de finalización aparte (la señal de un evento o un paquete en el puerto de finalización de E/S), así que lo más seguro es concentrar el tratamiento del resultado en el lado de la notificación.
- ¿Se puede reutilizar una estructura OVERLAPPED?
- No se debe compartir entre varias operaciones simultáneas. La estructura OVERLAPPED representa «el estado de una operación en curso», y la documentación de Microsoft indica con claridad que si se emiten tres E/S hacen falta tres estructuras OVERLAPPED, y que reutilizarlas lleva a resultados impredecibles o a la corrupción de datos. Hasta que la operación se complete hay que mantener válidos tanto la estructura como el búfer de lectura o escritura, y no se debe tocar su contenido. Si se reutiliza después de que se complete, hay que reinicializarla cada vez para que no influyan los datos que quedaron de la vez anterior. Para hEvent, lo más seguro es usar un evento de reinicio manual.
- ¿Cómo se cancela una E/S que ya está en curso?
- CancelIoEx permite solicitar la cancelación de la E/S no completada de un handle determinado, sin importar qué hilo la emitió. Si se pasa un OVERLAPPED como segundo argumento se apunta a una sola operación concreta; con NULL, a todas las operaciones de ese handle. El antiguo CancelIo solo puede cancelar «las operaciones que emitió el propio hilo que lo llama». Lo importante es que la cancelación es una «solicitud», no una «garantía» inmediata. Una operación que ya estaba a punto de completarse puede terminar con normalidad, y una operación cancelada se notifica como completada con ERROR_OPERATION_ABORTED. En ambos casos, hasta que llegue la notificación de finalización no se debe liberar la estructura OVERLAPPED ni el búfer. Para un hilo que está bloqueado en E/S síncrona desde otro hilo existe una API dedicada, CancelSynchronousIo.
- ¿Qué ocurre si no se especifica FileOptions.Asynchronous (useAsync) en FileStream de .NET?
- El handle se abre en modo síncrono, así que aunque se llame a ReadAsync/WriteAsync no se obtiene una E/S asíncrona real: un hilo del grupo de subprocesos se hace cargo de la lectura o escritura síncrona, y aparece una «asincronía aparente». El hilo que llama no queda bloqueado, pero por detrás hay otro hilo esperando, lo que puede agotar el grupo de subprocesos y reducir la escalabilidad. A la inversa, si se abre en modo asíncrono y luego se llama a Read/Write síncronos, se incurre internamente en el coste de esperar la finalización. El principio es hacer coincidir «el modo del handle» con «la API que se llama»; desde .NET 6, File.OpenHandle junto con RandomAccess permite escribirlo de forma directa, indicando de forma explícita el modo y el desplazamiento (offset).
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.