Despertares espurios — por qué una variable de condición despierta «sin notificación» y cómo esperar correctamente en Windows
· Actualizado el: · Go Komura · Windows, Multithreading, Variables de condición, Sincronización, C++, C#, Win32 API, Investigación de fallos
Historial de revisiones (primera versión, publicada el 22 Aug 2026)
- Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22176631)
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). Despertares espurios — por qué una variable de condición despierta «sin notificación» y cómo esperar correctamente en Windows. KomuraSoft LLC. https://comcomponent.com/es/blog/windows-condition-variable-spurious-wakeup/
- DOI (archivo registrado)
- 10.5281/zenodo.22176631
- DOI (última versión registrada)
- 10.5281/zenodo.22176632
«Hemos metido los datos y luego notificado, y aun así el trabajador lee una cola vacía y falla.» «Hemos notificado, y aun así de vez en cuando el hilo no vuelve de la espera.» Ambos son fallos de encuentro, pero los sitios que hay que investigar son distintos.
El centro de este artículo es un principio: volver del wait de una variable de condición no garantiza que se cumpla la condición que se esperaba. Cuando se entienden el despertar espurio, que vuelve sin notificación, y el despertar robado, en el que la condición se consume después de la notificación, queda claro por qué hace falta while y no if. El despertar perdido, en el que se pierde una notificación, se trata aparte de estos dos.
| Lo que quiere saber o resolver | Dónde leer |
|---|---|
| Por qué una espera despierta sin notificación, y en qué se diferencia de un despertar robado | Qué se garantiza en el momento de volver, Por qué la especificación lo permite |
| Comprobar el código correcto para Win32, C++ y C# | La forma básica por lenguaje |
| Se indicó un tiempo de espera, pero la espera se alarga | Espera basada en un plazo |
| Un hilo no vuelve aunque se le notificó | Despertar perdido y PulseEvent |
| Investigar un fallo o un cuelgue que solo ocurre de vez en cuando | Pasos de investigación según el síntoma |
Este artículo se dirige a desarrolladores que escriben aplicaciones de negocio y software de control de equipos en Windows. Confirma el mecanismo a partir de fuentes primarias y lo conecta con implementaciones en Win32 (C), C++ y C#.
1. Primero la conclusión
El código de espera debe cumplir tres reglas.
| Principio | Lo que el código debe respetar |
|---|---|
| Esperar un estado, no una notificación | Hacer de la condición «¿la cola no está vacía?» y similares, no «¿me despertaron?» |
| Volver a comprobar la condición cada vez que se vuelve | while (!condición) wait(...); en C++, usar la sobrecarga con predicado wait(lock, pred) |
| Proteger la comprobación y la actualización con el mismo bloqueo | Actualizar el estado antes de notificar, para que ninguna notificación se cuele en el hueco entre la comprobación y la espera |
Win32, C++ y POSIX permiten, por especificación, despertares no ligados a una notificación explícita. Además, incluso cuando llega una notificación, otro hilo puede consumir la condición primero (un despertar robado), así que la nueva comprobación es obligatoria. Monitor.Wait de C# se usa con la misma disciplina, teniendo en cuenta los despertares robados.12345
La otra precaución es no recrear la notificación transitoria de una variable de condición con un pulso sobre un evento. PulseEvent en particular puede perder una notificación, y Microsoft indica no usarlo en aplicaciones nuevas y usar una variable de condición en su lugar.6
El resto del artículo sigue este orden: cómo funcionan los despertares (secciones 2 a 4), la implementación correcta (sección 5), los patrones que hay que evitar y la investigación (secciones 6 y 7), y una lista de comprobación (sección 8).
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 (17 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. Qué es un despertar espurio — «despertado» no significa «la condición se cumple»
2.1 Una variable de condición libera el bloqueo, espera y lo readquiere antes de volver
Una variable de condición (condition variable) es un primitivo de sincronización que hace esperar a un hilo hasta que se cumple una condición. En Win32 se usa la siguiente estructura y las siguientes API.1
| Función | Tipo o API de Win32 |
|---|---|
| Representa la variable de condición | CONDITION_VARIABLE |
| Libera el bloqueo y espera | SleepConditionVariableCS / SleepConditionVariableSRW |
| Despierta a los hilos en espera | WakeConditionVariable / WakeAllConditionVariable |
La API de espera libera de forma indivisible la sección crítica o el bloqueo SRW que sostiene el hilo y entra en la espera. Tras despertar, readquiere ese bloqueo antes de volver al llamador.1
Readquirir el bloqueo y volver, sin embargo, es otra cosa que el hecho de que se cumpla la condición que esperaba la aplicación.
2.2 Distinguir una notificación auténtica, un despertar espurio y un despertar robado
El tratamiento de los tiempos de espera se deja para la sección 5; primero se comparan los tres casos de despertar.
| Caso | Relación con una notificación explícita | Lectura en el momento de volver |
|---|---|---|
| Despertar auténtico | Existe una notificación | La condición suele cumplirse, pero el solo hecho de volver no lo garantiza |
| Despertar espurio | No ligado a una notificación explícita destinada a despertar este hilo | Vuelve aunque la condición siga sin cumplirse |
| Despertar robado (stolen wakeup) | Existe una notificación | Otro hilo consumió la condición primero, así que ya no se cumple |
flowchart TB
accTitle: Tres casos en los que wait vuelve
accDescr: El wait de una variable de condición vuelve no solo por una notificación auténtica, sino también por un despertar espurio sin notificación y por un despertar robado en el que llegó una notificación pero la condición se consumió primero, así que en todos los casos hay que volver a comprobar la condición
w["Vuelto de wait"] --> a["Notificación auténtica"]
w --> b["Despertar espurio (sin notificación)"]
w --> c["Despertar robado (condición ya consumida)"]
a --> r["Volver a comprobar la condición y luego continuar"]
b --> r
c --> r
Figura 1: Hay tres caminos de vuelta desde wait, y el llamador no puede distinguirlos, así que siempre hay que volver a comprobar la condición.
Los despertares espurios no se limitan al caso en que no se ha enviado ninguna notificación en todo el sistema. Cuando las notificaciones llegan en una ráfaga corta, la implementación puede, por motivos propios, despertar hilos en espera de más en un lote. Desde el punto de vista de un hilo que no tiene una notificación explícita correspondiente, eso también es un despertar espurio.
Microsoft Learn afirma que las variables de condición están sujetas tanto a despertares espurios como a despertares robados, y pide que se vuelva a comprobar el predicado, por lo general en un bucle while, después de que vuelva la espera. El predicado significa aquí la condición que se espera, por ejemplo «la cola no está vacía».1
2.3 Decidir si continuar a partir de la condición actual, no del motivo del despertar
El llamador no puede saber cuál de los tres lo trajo de vuelta. Así que la forma es: comprobar la condición misma cada vez que se vuelve, y esperar otra vez si no se cumple.
El while no impide el despertar espurio ni el despertar robado en sí. Está para que, ocurra el que ocurra, el procesamiento no continúe con la condición incumplida. Si se mantiene esta nueva comprobación y la protección con el mismo bloqueo de la sección 5, el código trata correctamente cada camino de despertar.
3. Por qué la especificación lo permite — una notificación precisa es cara
3.1 Para que no pague cada operación el coste de una notificación estricta
Una implementación que no produzca despertares espurios es posible en teoría. Que POSIX, Windows y C++ los permitan aun así se debe al rendimiento y a un diseño en el que quien espera vuelve a comprobar la condición. La Rationale de pthread_cond_wait de POSIX también explica esta decisión.3
Implementar de forma estricta una notificación que «despierte de forma fiable exactamente un hilo» añade un coste extra de sincronización a cada operación de la variable de condición, sobre todo en multiprocesadores. El programador se interpone entre la notificación y el despertar, y también hay interrupciones y expropiación. Permitir un despertar extra raro y volver a comprobar en el lado que espera mantiene la implementación más rápida.3
El bucle de nueva comprobación tiene otra ventaja: hace visible la intención en el código y lo vuelve más robusto. Si se trata una notificación no como «una garantía de que la condición se cumple» sino como una pista de que la condición puede haber cambiado, el código tolera también cambios en el lado que notifica, como despertar de más o despertar por lotes.3
3.2 Aunque no haya despertares espurios, el despertar robado permanece
Un despertar robado ocurre en el hueco de tiempo entre la notificación y la readquisición del bloqueo. Aunque el productor ponga un elemento en la cola y despierte al consumidor A, si el consumidor B toma ese elemento antes de que A readquiera el bloqueo, la cola está vacía cuando A vuelve.
sequenceDiagram
accTitle: Línea de tiempo de un despertar robado
accDescr: El productor pone un elemento en la cola y despierta al consumidor A que espera, pero antes de que A readquiera el bloqueo el consumidor B adquiere el bloqueo y toma el elemento, de modo que la cola está vacía cuando A despierta
participant A as Consumidor A (en espera)
participant P as Productor
participant B as Consumidor B
P->>P: Añadir un elemento a la cola
P->>A: WakeConditionVariable
Note over A: Despertado, espera a readquirir el bloqueo
B->>B: Adquirir el bloqueo y tomar un elemento
A->>A: Readquirir el bloqueo y volver de wait
Note over A: La cola está vacía (robada)
A->>A: Volver a comprobar en el while y esperar otra vez
Figura 2: Un despertar robado, en el que un tercer hilo consume la condición en el hueco de tiempo entre la notificación y el despertar, puede ocurrir en cualquier implementación.
Mientras un tercer hilo pueda tomar el bloqueo primero, pulir la implementación no basta para eliminar este despertar robado. Aunque se pudieran eliminar por completo los despertares espurios, el bucle de quien espera seguiría siendo necesario mientras existan despertares robados. Dado eso, la decisión de diseño es permitir los despertares espurios y mantener rápidas las variables de condición.
4. En qué capa aparece en Windows
4.1 Lo común es la disciplina de volver a comprobar; las razones del despertar se distinguen
| API o biblioteca | Por qué hace falta volver a comprobar |
|---|---|
| Variables de condición de Win32 | Los despertares espurios y los despertares robados están documentados de forma explícita2 |
WaitOnAddress |
Se le permite volver antes por motivos distintos de una señal a la dirección indicada7 |
C++ std::condition_variable |
Un wait sin predicado puede despertar de forma espuria48 |
.NET Monitor.Wait |
Otro hilo puede consumir la condición entre el despertar y la readquisición del bloqueo5 |
El ejemplo oficial de Win32 de una cola productor-consumidor también escribe la espera en un bucle while.9
WaitOnAddress, disponible a partir de Windows 8, es una API de bajo nivel que espera a que cambie el valor de una dirección indicada. Como ejemplos de volver antes sin una señal, la documentación oficial cita un estado de poca memoria, el abandono de un wake anterior para la misma dirección y la ejecución en una compilación checked. El ejemplo de uso es también un bucle while que vuelve a comparar el valor.7
4.2 El wait con predicado de C++ ejecuta el bucle por usted
La documentación de MSVC explica que wait(lock, pred) ejecuta de hecho lo siguiente.4
while (!Pred())
wait(Lck);
cppreference también afirma de forma explícita que un wait sin predicado puede volver de forma espuria. Con la forma con predicado, este bucle de nueva comprobación se puede dejar a la biblioteca.8
4.3 En C#, la atención recae en el despertar robado y la readquisición del bloqueo
Monitor.Wait / Pulse usan una cola de espera y una cola de listos. Un hilo despertado por Pulse / PulseAll pasa a la cola de listos y no sale de Wait hasta que readquiere el bloqueo. Que otro hilo pueda consumir la condición primero en ese intervalo es lo mismo que en Win32.510
Con Monitor.Wait de .NET, en lugar de suponer un despertar sin motivo del tipo de las variables de condición, se piensa por separado: hace falta la misma disciplina de while por los despertares robados y los tiempos de espera. La documentación también supone un uso en el que el hilo vuelve a evaluar la condición que lo hizo esperar y llama a Wait de nuevo si es necesario.5
flowchart TB
accTitle: Cada capa exige volver a comprobar el predicado
accDescr: En cada capa, C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE y el WaitOnAddress de bajo nivel, la documentación oficial exige volver a comprobar la condición después de despertar
cpp["C++ std::condition_variable"] --> rule["Al despertar, volver a comprobar la condición (while)"]
net[".NET Monitor.Wait"] --> rule
win["Win32 CONDITION_VARIABLE"] --> rule
woa["WaitOnAddress"] --> rule
Figura 3: Sea cual sea el lenguaje o el marco, cada capa del primitivo de espera exige de forma oficial una nueva comprobación después de despertar.
5. La forma correcta de esperar — escribirla con while y un predicado
La forma común: comprobar la condición y continuar solo cuando se cumple
Lo que se espera no es «si se recibió una notificación», sino un estado compartido protegido por un bloqueo. Comprobar y actualizar el recuento de la cola o un indicador dentro del mismo bloqueo, llamar a wait si la condición no se cumple y volver a comprobar al volver.
flowchart TB
accTitle: Flujo de un bucle de espera correcto
accDescr: Adquirir el bloqueo y comprobar la condición; si no se cumple, liberar el bloqueo y dormir; al despertar, readquirir el bloqueo y volver a la comprobación. Continuar con el bloqueo sostenido solo cuando la condición se cumple
l["Adquirir el bloqueo"] --> c{"¿Se cumple la condición?"}
c -->|"No"| s["wait (liberar el bloqueo y dormir)"]
s --> wk["Despertar (readquirir el bloqueo)"]
wk --> c
c -->|"Sí"| go["Procesar con el bloqueo aún sostenido"]
Figura 4: Una espera correcta es un bucle, y no hay hueco entre comprobar la condición y procesar (ambas cosas ocurren mientras se sostiene el bloqueo).
Con esta forma, en el momento de salir del bucle se ha confirmado que la condición se cumple mientras aún se sostiene el bloqueo. Se consume el estado tal cual, así que no hay un hueco entre la comprobación y el procesamiento en el que otro hilo pueda interponerse.
La forma básica en Win32 (C)
CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0; // estado compartido protegido por cs
// Inicializar una sola vez al arrancar (si la inicialización es estática, cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);
// Lado que espera (consumidor)
EnterCriticalSection(&cs);
while (queueCount == 0) { // siempre while, nunca if
SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// Aquí se sostiene el bloqueo y está garantizado queueCount > 0
--queueCount;
LeaveCriticalSection(&cs);
// Lado que notifica (productor)
EnterCriticalSection(&cs);
++queueCount; // actualizar el estado dentro del bloqueo
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv); // notificar después de liberar el bloqueo está bien
La distinción que hay que mantener es entre actualizar el estado y el lugar donde se llama a la notificación. ++queueCount debe ocurrir siempre dentro del bloqueo. WakeConditionVariable, en cambio, se puede llamar desde dentro o desde fuera del bloqueo. Microsoft indica que suele ser mejor despertar después de liberar el bloqueo, para reducir los cambios de contexto.1
La forma básica en C++ — tomar el wait con predicado por defecto
std::mutex m;
std::condition_variable cv;
std::queue<Item> q;
// Lado que espera
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); }); // internamente while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();
// Lado que notifica
{
std::lock_guard<std::mutex> lk(m);
q.push(std::move(item));
}
cv.notify_one();
El código nuevo debería tomar por defecto el wait con predicado. Un while (q.empty()) cv.wait(lk); existente también es una forma correcta, así que no hace falta reescribirlo solo porque el bucle esté escrito a mano. El problema es if (q.empty()) cv.wait(lk);, que no vuelve a comprobar.
La forma con predicado solo se ocupa del bucle. La disciplina de proteger también, en el lado que notifica, el estado compartido que lee el predicado con el mismo mutex, y de actualizar el estado antes de notificar, sigue siendo necesaria.
La forma básica en C#
private readonly object _gate = new();
private readonly Queue<Item> _queue = new();
// Lado que espera
lock (_gate)
{
while (_queue.Count == 0) // siempre while, nunca if
{
Monitor.Wait(_gate);
}
var item = _queue.Dequeue();
}
// Lado que notifica
lock (_gate)
{
_queue.Enqueue(item);
Monitor.Pulse(_gate); // Pulse de Monitor solo puede llamarse dentro del bloqueo
}
Monitor.Wait / Pulse / PulseAll se llaman dentro de un bloque sincronizado que sostiene el bloqueo en cuestión. Fuera del bloqueo lanzan SynchronizationLockException. No hay que confundirlo con Win32, donde la notificación se puede sacar fuera del bloqueo.10
Espera con tiempo de espera — calcular el tiempo restante a partir de un plazo
Pasar cada vez el mismo valor de tiempo de espera alarga la espera cada vez que un despertar espurio vuelve. La forma es fijar primero el plazo y calcular el tiempo restante cada vez que se vuelve.
ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
ULONGLONG now = GetTickCount64();
if (now >= deadline) {
break; // tiempo de espera (la condición sigue incumplida)
}
if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
GetLastError() != ERROR_TIMEOUT) {
break; // si el fallo no es un tiempo de espera, dejar de esperar y salir
}
// ERROR_TIMEOUT recibe su comprobación final en la condición del while y en la del plazo
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
flowchart TB
accTitle: Flujo correcto de una espera con tiempo de espera
accDescr: Fijar primero el plazo y, cada vez que se despierta, comprobar la condición y el plazo; si el plazo no ha pasado, recalcular el tiempo restante y volver a la espera
d["Fijar el plazo"] --> c{"¿Se cumple la condición?"}
c -->|"Sí"| go["Pasar al procesamiento"]
c -->|"No"| t{"¿Ha pasado el plazo?"}
t -->|"Sí"| to["Tratar el tiempo de espera"]
t -->|"No"| w["Calcular el tiempo restante y esperar"]
w --> c
Figura 5: Una espera con tiempo de espera no vuelve a pasar «el mismo tiempo de espera»; recalcula el tiempo restante a partir del plazo.
En C++, este tratamiento se puede dejar a la sobrecarga con predicado de wait_until, que toma un instante absoluto. Como devuelve el valor final del predicado incluso en un tiempo de espera, se puede decidir según si la condición acabó cumpliéndose.4
6. Patrones que hay que evitar
6.1 Comprobar la condición una sola vez con if
Cuando ocurre un despertar espurio o un despertar robado, el procesamiento continúa con la condición incumplida. Se manifiesta como un fallo o una corrupción de datos raros: tomar de una cola vacía, leer datos no inicializados, una doble liberación. Pasarlo al while o al wait con predicado de la sección 5.
6.2 Comprobar o actualizar la condición fuera del bloqueo
Esto no es «despertar de más», sino un despertar perdido (lost wakeup): perder la notificación y dormir para siempre.
Si quien espera mira la condición fuera del bloqueo, decide «aún no» y quien notifica actualiza el estado y notifica antes de que quien espera entre en la espera, en ese instante no hay nadie esperando. Quien espera entra en la espera después de que la notificación ya se ha ido, y sigue esperando si no llega otra notificación.
sequenceDiagram
accTitle: Línea de tiempo de un despertar perdido
accDescr: Si quien espera comprueba la condición fuera del bloqueo y quien notifica actualiza el estado y notifica en el hueco antes de entrar en la espera, la notificación se envía a una variable de condición sin nadie esperando y desaparece, y quien espera sigue esperando una notificación que no volverá
participant W as Quien espera
participant N as Quien notifica
W->>W: Comprobar la condición fuera del bloqueo (no se cumple)
N->>N: Actualizar el estado y notificar
Note over N: Nadie espera en este instante
W->>W: Entrar en wait
Note over W: La notificación ya se fue, así que no despierta
Figura 6: Comprobar la condición fuera del bloqueo deja pasar una notificación por el hueco entre la comprobación y wait: un despertar perdido.
La razón de que la API de espera de una variable de condición haga indivisibles la liberación del bloqueo y la entrada en la espera es cerrar este hueco. Si se mantiene la disciplina de proteger la comprobación y la actualización de la condición con el mismo bloqueo y entrar en la API de espera sosteniendo el bloqueo, se evita este despertar perdido.1
6.3 Recrear una notificación transitoria con un pulso sobre un evento
Usar CreateEvent y SetEvent no es en sí un antipatrón. Los eventos tienen usos legítimos como los siguientes.
| Uso | Cuándo usar un evento |
|---|---|
| Despertar a un solo consumidor | Un diseño en el que el consumidor despertado procesa la cola hasta que queda vacía |
| Señalar una parada | Representar una instrucción de parada que, una vez alzada, no se vuelve a bajar, con un evento de restablecimiento manual |
| Esperar junto con otros destinos de espera | Incluirlo en WaitForMultipleObjects |
| Encuentro entre procesos | Un uso que una variable de condición, que no se puede compartir entre procesos, no puede cubrir |
Una variable de condición es un objeto de modo usuario que no se puede compartir entre procesos. Según el uso, sustituir el evento por una variable de condición ni siquiera es posible.1
Lo peligroso es intentar recrear con un evento el comportamiento de una variable de condición de «despertar solo a los hilos que esperan en ese instante y no dejar estado de notificación». Esa idea lleva al problema de PulseEvent que sigue.
6.4 Usar PulseEvent
PulseEvent es una API que, sobre un evento de restablecimiento manual, despierta a los hilos que esperan en ese instante y devuelve de inmediato el evento al estado no señalado. Microsoft, sin embargo, afirma de forma explícita que no es fiable y existe sobre todo por compatibilidad hacia atrás, así que las aplicaciones nuevas no deben usarla y deben usar una variable de condición en su lugar.6
La razón es que un hilo en espera puede ser retirado temporalmente del estado de espera por un APC de modo kernel y volver a la espera cuando el APC termina. Si se llama a PulseEvent en ese intervalo, ese hilo no está entre «los que esperaban en el momento de la llamada» y no se despierta. Los APC de kernel son un comportamiento interno del sistema operativo que la aplicación no puede controlar.611
Este problema es también el aviso de análisis estático C28648.12 Un despertar espurio es el problema de «despertar de más»; este es el de «no despertar cuando debería». Como se pierde la notificación misma, envolver la espera en un while no basta para salvarlo.
6.5 Notificar sin sostener el bloqueo, antes de actualizar el estado
Si se notifica primero sin sostener el bloqueo y luego se toma el bloqueo y se actualiza el estado, el lado despertado puede ver el estado antiguo y volverse a dormir. Si no hay una notificación después de la actualización, se queda ahí.
Hay que distinguir, sin embargo, los dos casos siguientes.
| Orden | Resultado |
|---|---|
| Notificar fuera del bloqueo y luego tomar el bloqueo y actualizar el estado | Hay un hueco en el que el lado despertado vuelve a comprobar antes de la actualización y se duerme |
| Notificar, actualizar y luego liberar, todo sosteniendo el mismo bloqueo | Quien espera no puede comprobar hasta que readquiere el bloqueo, así que este orden no causa un daño real |
En lugar de tener que examinar cada vez la seguridad del segundo, es más claro y más seguro unificarse en el orden actualizar el estado dentro del mismo bloqueo y luego notificar.
7. Cómo investigar cuando se encuentra
7.1 Separar «continúa aunque la condición no se cumple» de «no despierta»
| Síntoma | Qué comprobar primero | Cómo investigar |
|---|---|---|
| Tomar de una cola vacía, un fallo, resultados que faltan | Si se vuelve a comprobar la condición después de la espera | Buscar alrededor de cv.wait( sin predicado, SleepConditionVariableCS y Monitor.Wait |
| Un hilo que debería despertar no vuelve y el proceso se cuelga | Si hay un despertar perdido o un PulseEvent |
Comprobar la pila de cada hilo en un volcado y rastrear desde la API de espera en la que está parado hasta el lado que notifica |
Encontrar un cv.wait(lk) sin predicado no es un problema si está envuelto en un while correcto. Reunir candidatos con una búsqueda y revisar si el entorno es solo un if y si la comprobación y la actualización de la condición se hacen bajo el mismo bloqueo. Son sitios que se pueden inspeccionar sin esperar una reproducción.
En un cuelgue, identificar el lugar de espera a partir de un volcado y seguir en el código quién debía notificar y en qué orden. Comprobar comprobaciones de condición fuera del bloqueo, notificaciones fuera del bloqueo antes de actualizar el estado y PulseEvent.
flowchart TB
accTitle: Flujo de triaje a partir del síntoma
accDescr: Si el síntoma es que el procesamiento continúa con la condición incumplida, buscar en el código esperas sin predicado; si el síntoma es un hilo que no despierta, identificar el lugar de espera a partir de un volcado y sospechar un despertar perdido o PulseEvent
s["Un fallo que solo aparece de vez en cuando"] --> a["El procesamiento continúa con la condición incumplida"]
s --> b["Un hilo que debería despertar no despierta"]
a --> a1["Buscar en el código esperas sin predicado"]
b --> b1["Identificar los hilos en espera a partir de un volcado"]
a1 -.-> a2["Cambiar if por while o un wait con predicado"]
b1 -.-> b2["Sospechar un despertar perdido y PulseEvent"]
Figura 7: Que el síntoma sea «ir demasiado lejos» o «no despertar nunca» decide tanto dónde buscar como con qué medios investigar.
7.2 Comparar antes y después de la corrección bajo las mismas condiciones de estrés
Para reproducir un fallo raro se ensancha la ventana de carrera y se aumenta la dispersión de los tiempos. Los métodos incluyen usar más hilos que núcleos físicos, insertar un Sleep de diagnóstico entre la espera y la notificación, y probar compilaciones de depuración y de publicación.
Cuando se compara para concluir «dejó de reproducirse después de corregir la espera», aplicar el mismo estrés antes y después de la corrección.
8. Resumen — lista de comprobación
Una notificación es una pista de que la condición puede haber cambiado; el fundamento para continuar es la condición misma, comprobada dentro del bloqueo.
| Punto de comprobación | Forma correcta |
|---|---|
| Después de volver de la espera | No intentar distinguir una notificación auténtica, un despertar espurio y un despertar robado; volver a comprobar la condición |
| Cómo se escribe la espera | Envolverla en while. El código C++ nuevo toma por defecto el wait(lock, pred) con predicado |
| Estado compartido | Proteger la comprobación y la actualización con el mismo bloqueo, y actualizar el estado antes de notificar |
| Dónde llamar a la notificación | Win32/C++ pueden notificar después de liberar el bloqueo. El Pulse de C# se llama dentro del bloqueo |
| Tiempo de espera | Calcular el tiempo restante a partir de un plazo. En C++, usar wait_until con predicado |
| Uso de eventos | Distinguir los usos legítimos de los pulsos transitorios, y no depender de PulseEvent |
Los despertares espurios son una especificación que Win32, C++ y POSIX permitieron de forma deliberada. La respuesta es la disciplina de quien espera, no esperar una corrección del sistema operativo ni cambiar de biblioteca. Monitor.Wait de .NET se distingue de los despertares sin motivo, pero hace la misma nueva comprobación para hacer frente a los despertares robados y a los tiempos de espera.
Si es un fallo, empezar por «el sitio que continúa aunque la condición no se cumple»; si es un cuelgue, por «el sitio que pierde la notificación». Usar también esta distinción y la lista de comprobación al revisar código existente.
Artículos relacionados
- Buenas prácticas de multithreading en la práctica — Edición C++
- Buenas prácticas de multihilo en la práctica — Edición C
- Buenas prácticas de multithreading en la práctica — Edición .NET
- Por qué priorizar la espera por eventos sobre Sleep(1) en Windows
- Trampas de la memoria compartida y buenas prácticas para producción
- Las profundidades de la E/S de Windows (Parte 2) ── E/S síncrona y E/S asíncrona: el verdadero significado de OVERLAPPED
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa de revisiones de diseño de código multihilo, de la investigación de causas (análisis de volcados) de fallos y cuelgues que «solo se reproducen de vez en cuando», y de rehacer código de sincronización legado (dependiente de eventos, PulseEvent y similares) sobre una base de variables de condición. Empezar por aislar el síntoma está bien; no dude en ponerse en contacto.
- Consultoría técnica y revisión de diseño
- Investigación de errores y análisis de causa raíz
- Desarrollo de aplicaciones para Windows
- Contacto
Referencias
-
Microsoft Learn, Condition Variables. Sobre que una variable de condición es un objeto de modo usuario que libera de forma indivisible el bloqueo y entra en la espera; sobre que hay despertares espurios (despertares no ligados a un despertar explícito) y despertares robados (otro hilo se ejecuta antes que el hilo despertado), de modo que el predicado debería volver a comprobarse en un bucle while después de volver de la espera; y sobre que la notificación es posible desde dentro o desde fuera del bloqueo, pero despertar después de liberar el bloqueo es mejor para reducir los cambios de contexto. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, SleepConditionVariableCS function (synchapi.h). Sobre liberar de forma indivisible la sección crítica indicada y esperar en la variable de condición; sobre que el hilo despertado readquiere la sección crítica antes de volver; sobre que se devuelve ERROR_TIMEOUT ante un tiempo de espera; y sobre que hay despertares espurios y despertares robados, de modo que el predicado debería volver a comprobarse (por lo general en un bucle while) después de volver de la espera. ↩ ↩2
-
The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. Sobre que pueden ocurrir despertares espurios desde pthread_cond_wait / pthread_cond_timedwait; sobre que volver de wait no dice nada del valor del predicado, así que el predicado debería reevaluarse; y sobre que la Rationale afirma que una implementación que «despierta exactamente uno» puede ralentizar las operaciones de variable de condición, sobre todo en multiprocesadores, y que permitir despertares espurios impone un bucle de comprobación del predicado y hace las aplicaciones más robustas. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, condition_variable Class. Sobre que wait sin predicado se afirma que se desbloquea con notify_one / notify_all y también que puede despertar de forma espuria; sobre que wait(lock, pred) con predicado ejecuta de hecho while (!Pred()) wait(Lck);; y sobre que wait_for / wait_until tienen la misma propiedad y sobrecargas con predicado. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Monitor.Wait Method. Sobre que Wait libera el bloqueo y entra en la cola de espera; sobre que después de ser despertado por Pulse / PulseAll no vuelve hasta que se readquiere el bloqueo; y sobre que el uso previsto es que el hilo despertado vuelva a evaluar la condición que lo hizo esperar y llame a Wait de nuevo si es necesario. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, PulseEvent function (winbase.h). Sobre que un hilo en espera puede ser retirado temporalmente del estado de espera por un APC de modo kernel y volver después de que el APC termine, de modo que el hilo no se libera si se llama a PulseEvent en ese intervalo; y sobre que PulseEvent es por tanto poco fiable, no debe usarse en aplicaciones nuevas y debe sustituirse por una variable de condición. ↩ ↩2 ↩3
-
Microsoft Learn, WaitOnAddress function (synchapi.h). Sobre que la función que espera a que cambie el valor de una dirección está garantizada a volver cuando se señala, pero también se le permite volver por otros motivos; sobre que los ejemplos de despertar pronto son un estado de poca memoria, el abandono de un wake anterior para la misma dirección y la ejecución en una compilación checked; y sobre que por tanto hay que volver a comparar el valor después de volver, siendo el propio ejemplo oficial un bucle while. ↩ ↩2
-
cppreference.com, std::condition_variable::wait. Sobre que wait sin predicado puede desbloquearse por un despertar espurio; y sobre que la sobrecarga con predicado es equivalente a while (!pred()) wait(lock); y se define como un bucle que readquiere el bloqueo y comprueba el predicado en cada notificación o despertar espurio. ↩ ↩2
-
Microsoft Learn, Using Condition Variables. Sobre el ejemplo oficial que implementa una cola productor-consumidor con una sección crítica y dos variables de condición (BufferNotEmpty y BufferNotFull). La espera se realiza dentro de un bucle que comprueba el predicado. ↩
-
Microsoft Learn, Monitor.PulseAll Method. Sobre que PulseAll mueve los hilos de la cola de espera a la cola de listos, y el siguiente hilo de la cola de listos adquiere el bloqueo cuando se libera; y sobre que Pulse / PulseAll / Wait solo pueden llamarse desde un bloque sincronizado. ↩ ↩2
-
Microsoft Learn, Waits and APCs. Sobre que los APC de kernel se ejecutan de forma expropiativa, y el sistema interrumpe y reanuda la espera internamente sin volver de la API de espera, de modo que una señal transitoria como KePulseEvent puede perderse en ese intervalo. ↩
-
Microsoft Learn, C28648: PulseEvent is an unreliable function. Sobre que el análisis estático avisa del uso de PulseEvent; sobre que un hilo que estaba fuera de la espera por un APC no se libera y puede colgarse de forma permanente; y sobre la guía para sustituirlo por SetEvent u otro objeto de sincronización. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
DllMain y el bloqueo del cargador — La razón real de que le digan «no haga nada en la inicialización de la DLL»
Por qué no debe llamar a LoadLibrary ni sincronizar con otros hilos desde DllMain. A partir de fuentes primarias, este artículo explica e...
Por qué se rompen los argumentos — Las reglas de los argumentos de línea de comandos de Windows
Windows pasa a CreateProcess una sola cadena que el receptor divide. Cubre las reglas de CommandLineToArgvW, el CRT y .NET, ArgumentList ...
Qué queda después de que muere el padre — mantener los procesos hijos en un Job Object
Por qué los ayudantes del SDK sobreviven a una IU terminada y retienen la cámara o el puerto COM. Cómo diseñar la vida de los procesos hi...
API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork
¿Está multiplicando las llamadas a CreateThread en el código nativo? Este artículo explica, a partir de fuentes primarias, la API del gru...
Canalizaciones con nombre en la práctica — El IPC estándar de Windows, del diseño a la seguridad
Guía práctica de las canalizaciones con nombre, el IPC estándar de Windows. Organiza, a partir de fuentes primarias, la elección entre mo...
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.
Investigación de fallos y problemas prolongados
Fallos intermitentes, diagnóstico de comunicaciones, bloqueos prolongados y pruebas de rutas de error.
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.
- ¿Es un despertar espurio un error del sistema operativo o de la biblioteca?
- No es un error; es un comportamiento escrito en la especificación. SleepConditionVariableCS de Win32, std::condition_variable de C++ y pthread_cond_wait de POSIX tienen todos documentación oficial o un estándar que afirma de forma explícita que puede ocurrir un despertar no ligado a una notificación. Una implementación que lo prohibiera es teóricamente posible, pero ralentizaría todas las operaciones de la variable de condición (sobre todo la notificación en multiprocesadores). Por eso se tolera el comportamiento con el criterio de que la corrección se conserva si quien espera vuelve a comprobar la condición. El remedio, por tanto, no es esperar una corrección del sistema operativo, sino escribir siempre wait en un bucle while (o un wait con predicado).
- ¿Envolver wait en un while perjudica el rendimiento?
- En la práctica el coste es despreciable. Lo único que añade el bucle while es una comprobación de la condición cada vez que se despierta, y eso es una comparación ligera mientras se sostiene el bloqueo. Los propios despertares espurios son raros, así que la iteración extra del bucle solo ocurre en casos excepcionales. El precio de dejar un if, en cambio, es un error que solo se reproduce raramente, en el que el procesamiento continúa con la condición incumplida; no hay comparación. Lo que de verdad determina el coste de espera de una variable de condición es la contención del bloqueo y la frecuencia de las notificaciones, no si está el while.
- Si uso el wait con predicado de C++, ¿puedo dejar de pensar en los despertares espurios?
- Para el bucle de espera, sí: cv.wait(lock, pred) ejecuta de hecho while (!pred()) wait(lock);, de modo que los despertares espurios y los despertares robados se absorben de forma automática. El código C++ nuevo debería tomar por defecto la sobrecarga con predicado. Aun así hay que proteger con el mismo mutex las actualizaciones del estado compartido que lee el predicado, y quien notifica sigue teniendo que actualizar el estado antes de llamar a notify. El wait con predicado se ocupa del bucle, no de la disciplina de bloqueos.
- ¿Ocurre el mismo problema con Monitor.Wait de C#?
- Sí. Un hilo que espera en Monitor.Wait lo despierta Pulse/PulseAll y luego readquiere el bloqueo antes de salir de Wait, pero en ese intervalo otro hilo puede haber adquirido el bloqueo primero y haber consumido la condición (un despertar robado). La documentación de Microsoft también parte del supuesto de que el hilo despertado vuelve a evaluar la condición que lo hizo esperar y llama a Wait de nuevo si es necesario. Así que la forma básica en C# es también while (!condición) Monitor.Wait(gate);. Que Wait/Pulse solo puedan llamarse desde dentro de una instrucción lock es una restricción que difiere de Win32.
- ¿También hay despertares espurios al esperar un evento con WaitForSingleObject?
- En una espera ordinaria (no alertable), WAIT_OBJECT_0 se devuelve solo cuando el objeto pasa de verdad a señalado, así que no hay un despertar sin motivo del tipo que tienen las variables de condición. Sin embargo, «el evento pasó a señalado» y «se cumple la condición de la aplicación» son cosas distintas. En un diseño en el que varios consumidores los despierta el mismo evento, el hilo que toma el bloqueo primero consume la condición, así que hace falta una comprobación de la condición después de despertar. Además, un diseño que intenta reproducir con un evento la notificación transitoria de una variable de condición, que solo despierta a quienes esperan en ese instante, tiende a acabar en el problema de fiabilidad de PulseEvent. Para esperar a que un estado se cumpla dentro de un proceso, una variable de condición es la opción segura.
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.