Buenas prácticas de multithreading en la práctica: edición C — escribir con seguridad al estilo de la API Win32
· Actualizado el: · Go Komura · Windows, Multithreading, Lenguaje C, Win32 API, Aplicaciones empresariales, Investigación de fallos, Diseño
Historial de revisiones (primera versión, publicada el 2 Aug 2026)
- Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22175889)
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). Buenas prácticas de multithreading en la práctica: edición C — escribir con seguridad al estilo de la API Win32. KomuraSoft LLC. https://comcomponent.com/es/blog/multithreading-best-practices-c/
- DOI (archivo registrado)
- 10.5281/zenodo.22175889
- DOI (última versión registrada)
- 10.5281/zenodo.22175890
«Un proceso residente escrito en C se queda colgado solo al terminar», «quiero añadir hilos a una aplicación antigua de control de equipos», «detengo los hilos con TerminateThread, pero de vez en cuando deja de responder todo el proceso». Cuando se escribe multithreading en C, el problema no es tanto poner trabajo en paralelo como quién protege los datos compartidos, cómo se espera y cómo se termina.
C no tiene un mecanismo como el RAII de C++ que libere un bloqueo o un identificador al salir del ámbito. Tampoco hay apoyo de excepciones ni de plantillas, así que no basta con elegir la API: la disciplina de adquirir, liberar y esperar la salida hay que incorporarla a la estructura del código.
Este artículo es la edición C de la serie práctica sobre multithreading. Dirigido a quienes usan C y la API Win32, traduce a API concretas los principios de no fabricar hilos a la ligera, reducir el estado mutable compartido, fijar la disciplina de los bloqueos y diseñar primero cómo se detiene.
Se cubre la creación con _beginthreadex, la elección de objetos de sincronización, la parada cooperativa que no se apoya en TerminateThread y las restricciones de DllMain. La información está vigente a agosto de 2026. Se puede leer de forma independiente, pero los mismos principios aplicados a otros lenguajes están en la «edición .NET», la «edición C++» y la «edición Java».
Leer a partir de lo que está fallando
| Lo que está fallando | Qué comprobar primero | Dónde leer |
|---|---|---|
| Quiero añadir hilos / ejecutar en masa trabajo corto | Cuándo usar hilos propios y cuándo el grupo de subprocesos | Cómo crear hilos |
| Los contadores no cuadran / se corrompen los datos compartidos | El diseño que reduce el compartir, y la correspondencia de bloqueos y operaciones atómicas | Reducir el estado compartido·Cómo elegir la sincronización |
| Al poner un bloqueo se quedó colgado | Los datos que protege, el trabajo mientras se retiene y el orden de adquisición | Disciplina de los bloqueos |
| Se cuelga al terminar / se usa terminación forzada | Separar la petición de parada de la confirmación de salida, y si un hilo en espera aún puede detenerse | Parada cooperativa |
| Se despierta con un evento, pero el trabajo se desequilibra | La diferencia entre el aviso a un solo trabajador y a varios | Alcance del ejemplo de detención |
| Se detiene al descargar la DLL | Si el arranque, la detención y la reunión se hacen fuera de DllMain | Restricciones de DllMain |
| Quiero compartir código con Linux / no puedo reproducir un fallo | Los requisitos de portabilidad y la revisión de diseño, los registros y la prueba de carga | La opción C11·Verificación y depuración |
Si se diseña por primera vez, se cubren las premisas de C en el capítulo 2, se deciden las unidades de ejecución y los datos compartidos en los capítulos 3 a 5, y se comprueba el camino de detención en el 6. Para revisar código existente también sirve la lista de verificación del final.
1. Primero la conclusión
Elegir dónde se ejecuta según la naturaleza del trabajo
Los hilos propios que llaman al CRT se crean con _beginthreadex. Si un hilo creado con CreateThread llama al CRT, en baja memoria el CRT puede terminar el proceso. Para paralelizar en masa trabajo corto, en lugar de crear hilos propios una y otra vez se usa CreateThreadpoolWork del grupo de subprocesos de Windows.123
No se debe terminar un hilo del grupo con ExitThread ni con TerminateThread. Es un lugar de ejecución prestado como devolución de llamada: se limpia y se devuelve.4
Separar la protección de los datos compartidos de la espera
Dentro del proceso, el bloqueo por defecto es el bloqueo SRW; se elige CRITICAL_SECTION cuando hace falta adquisición recursiva. Usar un Mutex para una exclusión frecuente dentro del proceso paga el coste de las transiciones al kernel.5
La actualización de una sola variable se hace con las funciones de la familia Interlocked. Añadir volatile no asegura por sí solo la atomicidad ni la sincronización necesaria. La mayoría de las funciones Interlocked llevan una barrera de memoria completa. Para esperar se usa una variable de condición, o un evento con una función de espera, de modo que el sondeo con Sleep no desperdicie CPU ni capacidad de respuesta.67
Decidir cómo detener y el orden de liberación antes de arrancar
No se usa TerminateThread; se diseña una parada cooperativa con un evento de detención y WaitForMultipleObjects. La terminación forzada puede romper el estado de bloqueos, del montón y de las DLL, y es el objetivo de la advertencia de análisis de código C6258. No se pasa a liberar recursos solo porque se pidió la parada: el identificador se cierra después de confirmar que el hilo ha terminado.89
En DllMain no se crean hilos, no se sincroniza y no se espera a que terminen. Las funciones explícitas de inicialización y de cierre se ponen fuera del bloqueo del cargador.10
Si hace falta portabilidad, C11 también es una opción. A agosto de 2026, <threads.h> se puede usar desde VS 2022 17.8 y <stdatomic.h> es experimental. En código exclusivo de Windows, la línea de este artículo con la API Win32 es la base.11
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 (23 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. Por qué el multithreading es difícil: condiciones de carrera e interbloqueos
Lo primero que hay que fijar es la condición de carrera, en la que el resultado cambia con el orden de ejecución, y el interbloqueo, en el que un ciclo de esperas impide avanzar. Hay que distinguirlos antes de aprender las API.
Una condición de carrera (race condition) es un error en el que el resultado cambia según el orden en que varios hilos llegan a un fragmento concreto de código.
Si se piensa en count++ de un contador compartido, aunque sea una sola expresión, sin sincronización no hay garantía de que «leer → sumar → escribir de vuelta» se ejecute de forma indivisible. Si dos hilos leen el mismo valor, cada uno suma y cada uno escribe de vuelta, se pierde uno de los incrementos.6 La figura 1 muestra un orden de ejecución en el que se pretendía incrementar dos veces desde 10 y aun así queda 11.
sequenceDiagram
participant A as Hilo A
participant M as Variable compartida count
participant B as Hilo B
Note over M: count = 10
A->>M: Lectura(10)
B->>M: Lectura(10)
A->>A: Suma en local(11)
B->>B: Suma en local(11)
A->>M: Escritura de vuelta(11)
B->>M: Escritura de vuelta(11)
Note over M: Se sumó dos veces, pero count = 11<br/>Se perdió el incremento del hilo A
Figura 1: La condición de carrera típica en la que un contador compartido pierde un incremento. Si otro hilo se cuela entre los tres pasos de count++, quien escribe de vuelta después sobrescribe al otro.
Un interbloqueo es el estado en el que dos hilos se esperan mutuamente por el bloqueo que tiene el otro y ninguno puede avanzar. El hilo A retiene el bloqueo 1 y espera el 2, el hilo B retiene el 2 y espera el 1: con eso ambos se detienen para siempre.
flowchart LR
A["Hilo A<br/>reteniendo el bloqueo 1"] -->|"espera la liberación del bloqueo 2"| B["Hilo B<br/>reteniendo el bloqueo 2"]
B -->|"espera la liberación del bloqueo 1"| A
Figura 2: La espera circular del interbloqueo. En el instante en que las flechas de espera forman un anillo, todos los hilos del anillo se detienen para siempre.
Estos fallos dependen del momento. Un orden de ejecución que casi no aparece en la máquina de desarrollo puede aparecer una y otra vez en el cliente, con otro número de núcleos y otra carga. Que deje de reproducirse al conectar un depurador se debe a que la observación cambia el momento.
Por eso todos los principios que siguen empiezan por «reducir los lugares que necesitan sincronización» antes de «sincronizar correctamente».
2.1. La premisa propia de C: el lenguaje no protege de nada
En C, como el lenguaje no tiene un mecanismo que haga cumplir estos principios, hay que dejarlos por escrito como disciplina.
Diseñar la liberación incluyendo todas las salidas de la función
No hay un equivalente del RAII de C++, así que la liberación de bloqueos y el CloseHandle de los identificadores los garantiza uno mismo. Se usa el patrón goto cleanup que concentra todas las salidas de la función en un solo sitio, o el convenio de escribir cada adquisición emparejada con su liberación. El punto es una estructura en la que añadir más tarde un return temprano no pueda saltarse la liberación.
Aunque la lectura y la escritura simples sean atómicas, la sincronización hace falta por separado
La necesidad de evitar condiciones de carrera de datos es la misma que en C++. En Windows, la lectura o escritura simple de una variable de 32 bits correctamente alineada es atómica, pero eso por sí solo no garantiza ni la sincronización del acceso ni el orden de las operaciones de memoria de alrededor. Una variable de 64 bits en Windows de 32 bits, una operación compuesta de leer y sumar, y la consistencia entre varias variables tampoco se pueden tratar como una lectura o escritura simple.12
No se use «resulta que funciona» como prueba: la sincronización necesaria se declara con un bloqueo o con Interlocked. Esta disciplina sigue haciendo falta aunque cambien el compilador o el nivel de optimización.
Escribir la propiedad hasta el momento del traspaso
En el comentario de la función se deja explícito «qué hilo escribe en este búfer» y «desde cuándo es de quién». La disciplina de la propiedad es tan importante como la elección del primitivo de sincronización. El procesamiento partido y las colas que se ven más adelante solo se usan con seguridad cuando esa frontera ya está decidida.
3. Cómo crear hilos: _beginthreadex como única opción
3.1. Por qué CreateThread no vale
La API nativa de Win32 es CreateThread, pero la pauta oficial es crear con _beginthreadex los hilos que llaman funciones del CRT (biblioteca en tiempo de ejecución de C). _beginthreadex inicializa los datos internos que el CRT usa por hilo y luego arranca la ejecución.2
Si un hilo creado con CreateThread llama funciones del CRT, en baja memoria el CRT puede terminar el proceso.1 printf, malloc y strtok también son CRT, así que en la práctica se puede tomar como base «en C, un hilo propio es _beginthreadex».
La responsabilidad de quien crea llega hasta esperar el identificador y cerrarlo
También se evita _beginthread (sin ex). Si el hilo termina pronto, el identificador devuelto puede quedar inválido y apuntar a otro hilo. Se elige _beginthreadex, que permite pasar el identificador a las API de sincronización, y quien llama espera la terminación y hace CloseHandle.13
El siguiente extracto muestra el flujo de creación y reunión. Se da por supuesto que la definición de WorkerContext, la inicialización de ctx y el tratamiento de fallos los prepara la aplicación: no es un programa completo que compile por sí solo. Si la creación falla no se pasa a esperar; si tiene éxito, el ctx que usa el trabajador se mantiene válido hasta confirmar la terminación.
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... bucle de espera de los capítulos 5 y 6 ... */
return 0;
}
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* tratamiento del fallo */ }
/* ...después de pedir la parada... */
WaitForSingleObject(hThread, INFINITE); /* reunión */
CloseHandle(hThread);
3.2. El trabajo corto, al grupo de subprocesos de Windows
Cuando «se quiere lanzar en masa trabajo pequeño» o «se crean y se destruyen una y otra vez hilos de vida corta», se usa el grupo de subprocesos de Windows. En la API de Windows Vista en adelante se crea un objeto de trabajo con CreateThreadpoolWork y se envía con SubmitThreadpoolWork. El trabajador del grupo ejecuta la devolución de llamada, de modo que el número de hilos lo gestiona el SO y se elimina la necesidad de crear y destruir un hilo propio por cada trabajo.3
Restaurar el estado del hilo prestado antes de devolverlo
La disciplina oficial es no terminar un hilo del grupo con TerminateThread / ExitThread, restaurar antes de volver el TLS, la prioridad del hilo y demás que se hayan cambiado en la devolución de llamada, y mantener vivos los identificadores de espera hasta que el grupo deje de usarlos.4
Gestionar el número de hilos no basta para limitar lo que se admite
Aunque se deje al grupo la gestión del número de hilos, eso no impide un diseño en el que las devoluciones de llamada no ejecutadas se acumulen sin límite. En un proceso residente donde las admisiones siguen superando el procesamiento, en el lado de la aplicación se pone un límite de entrada como un semáforo o una cola con capacidad.
Hay que decidir también si, al llenarse, el lado que admite espera o rechaza. Es contrapresión para no trasladar la sobrecarga a consumo de memoria, la misma idea que el búfer acotado del capítulo 5.
4. Minimizar el estado mutable compartido: partir, dejar de solo lectura, traspasar
Antes de elegir el primitivo de sincronización, se piensa si se pueden reducir los lugares en los que varios hilos tocan los mismos datos mutables. Los tres medios son partir, dejar de solo lectura y traspasar por una cola.
Partir por hilo y reunir al final
En una agregación paralela, en lugar de que cada hilo escriba cada vez en un contador compartido, se construye un subtotal en una variable local o en un búfer dedicado. Si al final se reúne una sola vez con InterlockedAdd o similar, las escrituras al compartir bajan de «en cada iteración» a «una vez por hilo».
Es un método que reduce tanto el coste de sincronización como las ocasiones de contención. El «qué hilo posee este búfer» decidido en el apartado 2.1 se convierte tal cual en el diseño de la partición.
Tras terminar la inicialización, dejarlo de solo lectura
La configuración y las tablas que se construyen al arrancar y no se reescriben después se pueden leer desde varios hilos una vez terminada la inicialización. Sin dejar la frontera ambigua, o se termina la inicialización antes de arrancar todos los hilos, o, si es diferida, se usa InitOnceExecuteOnce.5
Lo importante no es «la intención de solo lectura», sino dejar claro en el código cuándo termina la inicialización y desde cuándo no se reescribe.
Cambiar una variable compartida que tocan ambos lados por un traspaso por cola
El flujo de datos entre hilos se acerca más a una cola productor/consumidor que a operar una variable compartida desde ambos lados. En C y Win32 hay un ejemplo oficial de implementación que combina un búfer circular acotado con SleepConditionVariableCS.7
Si se pone un tope de capacidad, cuando la producción adelanta al consumo el lado que produce espera: contrapresión natural.
5. Cómo elegir el objeto de sincronización y la disciplina de los bloqueos
Los primitivos de sincronización de Win32 son muchos, y una mala elección daña tanto el rendimiento como la corrección. Se resume la pauta oficial en una sola hoja.5
flowchart TB
S{"¿Se sincroniza<br/>entre procesos?"} -->|"sí"| Q2{"¿Cuál es el uso?"}
Q2 -->|"exclusión"| MTX["Mutex con nombre"]
Q2 -->|"límite de accesos simultáneos"| SEM["Semáforo con nombre"]
Q2 -->|"aviso de un suceso"| EVT["Evento con nombre"]
S -->|"no (dentro del proceso)"| Q3{"¿Hace falta adquisición<br/>recursiva del mismo hilo?"}
Q3 -->|"sí"| CS["CRITICAL_SECTION"]
Q3 -->|"no"| Q4{"¿Código C++ que<br/>prioriza la portabilidad?"}
Q4 -->|"sí"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"no"| SRW["Bloqueo SRW (opción por defecto)"]
Figura 3: Cómo elegir el primitivo de sincronización de Win32. La primera ramificación es «¿se cruza el proceso?»; el punto es no elegir un objeto del kernel (Mutex) cuando no se cruza.
| Primitivo | Alcance | Rasgo | Dónde usarlo |
|---|---|---|---|
| Bloqueo SRW | Dentro del proceso | Rápido (normalmente termina en modo usuario), tamaño de un puntero, no recursivo | Valor por defecto en código nuevo. También permite lectura compartida con AcquireSRWLockShared |
| CRITICAL_SECTION | Dentro del proceso | Rápido (espera en el kernel tras un spin), recursivo | Cuando hace falta adquisición recursiva del mismo hilo |
| Mutex | Dentro del proceso / entre procesos | Siempre objeto del kernel y lento | Exclusión entre procesos (con nombre), combinación con WaitForMultipleObjects |
| Semáforo | Dentro del proceso / entre procesos | Objeto del kernel | Límite del número de accesos simultáneos a un grupo de recursos |
| Evento | Dentro del proceso / entre procesos | Objeto del kernel | Aviso de que «ocurrió algo» (no se usa para proteger datos) |
| Funciones Interlocked | Dentro del proceso (también entre procesos si es memoria compartida) | Operación atómica sin bloqueo | Contadores, banderas, intercambio de puntero6 |
Un objeto del kernel no es exclusivo de la comunicación entre procesos
Lo que muestra la figura 3 es el punto de no elegir un Mutex sin motivo para un bloqueo dentro del proceso. Para un aviso dentro del proceso se puede usar un evento; para limitar el número simultáneo, un semáforo. El evento de detención del capítulo 6 también es un objeto del kernel sin nombre.
Además, que no tenga nombre no implica que quede necesariamente limitado al proceso. Por herencia de identificadores o por duplicación con DuplicateHandle, el mismo objeto se puede usar desde varios procesos. Ponerle nombre es uno de los medios representativos para que otro proceso vuelva a abrir el mismo objeto.
Interlocked: separar la operación, la alineación y la vida
Hacer indivisible la operación sobre una sola variable
InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange son funciones que hacen indivisible la operación sobre una sola variable. Son la herramienta que corresponde a la clase Interlocked de la edición .NET y a std::atomic de la edición C++, y la mayoría de las funciones llevan también una barrera de memoria completa.6
No se debe pensar que basta con poner volatile para obtener atomicidad o la sincronización necesaria. Cuando hay que dejar consistentes varias variables a la vez, se protegen con un bloqueo SRW o una CRITICAL_SECTION.
Respetar la alineación de la variable objetivo
El objetivo de Interlocked debe estar alineado en un límite natural. Un valor de 32 bits, en un límite de 4 bytes; uno de 64 bits, en un límite de 8 bytes. El comportamiento si no está alineado no es predecible.12
No se tome como objetivo un campo de una estructura con #pragma pack ni de un búfer que mapea tal cual un formato de comunicación: los contadores y las banderas se preparan como variables normales que el compilador alinea de forma adecuada.
El intercambio de un puntero no protege la vida de los datos antiguos
Lo que InterlockedExchangePointer y similares hacen indivisible es el intercambio del puntero en sí. Si el lector obtiene el puntero antiguo y, justo después, quien escribe lo sustituye y hace free del bloque antiguo, el lector accede a memoria ya liberada.
Sustituir y recuperar son problemas distintos. Hace falta un procedimiento —bloqueo o un recuento de referencias seguro— para no recuperar el bloque antiguo hasta que el lector termine de usarlo. Si hay duda, se protege con un bloqueo SRW.
La variable de condición: al despertar, volver a comprobar la condición
En una cola productor/consumidor de búfer acotado se usa una variable de condición. La forma del ejemplo oficial de implementación es inicializar con InitializeConditionVariable, esperar en el lado que consume con SleepConditionVariableCS combinado con CRITICAL_SECTION, y despertar en el lado que produce con WakeConditionVariable.7 Si se combina con un bloqueo SRW se usa SleepConditionVariableSRW.
Lo importante es al despertar volver a comprobar la condición dentro del bloqueo y, si es falsa, volver a esperar en un bucle. Hay despertares espurios sin notificación, y también el caso de que, al despertar, otro consumidor ya se haya llevado el elemento. «Haber sido despertado» y «la cola no está vacía» no son lo mismo.
Es la herramienta para armar en C el mismo esquema que el canal del apartado 4.3 de la edición .NET y la BlockingQueue del capítulo 4 de la edición C++.
5.1. Disciplina de los bloqueos: tres principios que no cambian aunque se elija cualquiera
Aunque se elija bien el primitivo, sin disciplina de uso no se evitan las carreras.
1. Fijar la correspondencia entre datos y bloqueo
A cada conjunto de datos mutables que se protege se le hace corresponder un bloqueo SRW o una CRITICAL_SECTION. En todos los sitios que tocan esos datos se toma el mismo bloqueo.
En C resulta especialmente útil el convenio de escribir en el encabezado «esta estructura la protege g_lockFoo». En la revisión también se comprueba esa correspondencia en una tabla.
2. No hacer, mientras se retiene el bloqueo, trabajo largo ni llamadas externas
Dentro del bloqueo se reduce a leer y escribir los datos que se protegen. E/S de archivo, comunicación de red o llamadas a devoluciones de llamada mientras se retiene alargan el tiempo de retención. Si el destinatario adquiere otro bloqueo, también se puede crear la espera circular de la figura 2.
3. Fijar el orden de adquisición cuando hay varios bloqueos
Si se toman dos o más bloqueos, se define una jerarquía de bloqueos para que todos los hilos sigan el mismo orden. El documento de buenas prácticas de DLL también explica que invertir el orden de adquisición provoca interbloqueos y que hay que seguir la jerarquía de forma coherente.10
6. Diseñar cómo se detiene: no usar TerminateThread
6.1. Lo que rompe TerminateThread
TerminateThread termina el hilo objetivo sin dejarle ejecutar ninguna limpieza en modo usuario. El estado que retenía no tiene por qué quedar recogido de forma segura.8
| Momento en que se le termina | Lo que puede ocurrir |
|---|---|
| Mientras retiene una sección crítica | El bloqueo no se libera y los demás hilos siguen esperando |
| Mientras reserva memoria del montón | Queda el bloqueo del montón y las reservas posteriores se detienen |
| Mientras opera el estado global de una DLL | Se rompe el estado interno de la DLL |
Oficialmente se sitúa como «una función peligrosa que solo debe usarse en los casos más extremos», y el análisis de código la detecta como advertencia C6258.89
Que en la investigación de una aplicación que «de vez en cuando se queda colgada el proceso entero» aparezca TerminateThread es, en la práctica, un paisaje realmente frecuente. Si se encuentra, es objeto de reparación.
6.2. La forma correcta: evento de detención + WaitForMultipleObjects
En la parada cooperativa, quien detiene no borra el hilo: pide la parada y deja que el propio trabajador limpie y termine. La práctica establecida es crear un evento de detención de reinicio manual y que el trabajador espere a la vez la señal de trabajo y la de parada. El material oficial de C6258 también indica el método de vigilar un evento y terminar por cuenta propia.9
Trate como etapas distintas petición de parada → limpieza del trabajador → confirmación de que el hilo terminó → liberación de identificadores y recursos compartidos. Poner el evento de detención en estado señalizado no significa por sí solo que haya terminado.
El código siguiente también es un extracto para explicar. Se omiten la creación del evento y la comprobación de fallos, la exclusión de la cola, y las implementaciones de ProcessNextItem, LogLastError y Cleanup. El evento y la cola no se destruyen durante la espera ni el procesamiento, y se lee bajo el supuesto de aviso de trabajo orientado a un solo trabajador que se explica más abajo.
HANDLE hStopEvent; /* CreateEvent(NULL, TRUE, FALSE, NULL): reinicio manual */
HANDLE hWorkEvent; /* CreateEvent(NULL, FALSE, FALSE, NULL): reinicio automático.
Al recibirlo vuelve solo a no señalizado (si es de reinicio
manual, una vez señalizado la espera se salta y se entra
en un bucle ocupado sobre la cola vacía) */
static unsigned __stdcall WorkerMain(void* arg)
{
HANDLE waits[2] = { hStopEvent, hWorkEvent };
for (;;) {
DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (r == WAIT_FAILED) { /* identificador inválido, etc. Si se ignora, gira a toda velocidad */
LogLastError(); /* registrar GetLastError() y salir */
break;
}
if (r == WAIT_OBJECT_0) /* petición de parada */
break;
if (r == WAIT_OBJECT_0 + 1) { /* hay trabajo */
/* Pasar también el evento de detención a ProcessNextItem: si dentro
de un elemento se espera mucho, sin observarlo ahí el apagado
queda de rehén de ese elemento */
while (ProcessNextItem(hStopEvent)) { /* procesa 1 de la cola. Vacía → FALSE */
/* También durante la descarga se comprueba la petición de parada.
Si se omite, mientras siga llegando trabajo no se puede parar
(inanición de la detención) */
if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
break;
}
}
}
Cleanup(); /* la propia limpieza, uno mismo */
return 0; /* terminar por sí mismo */
}
BOOL StopWorkers(HANDLE* threads, DWORD count)
{
BOOL ok = TRUE;
if (!SetEvent(hStopEvent)) { /* si la petición de parada no llega, */
LogLastError(); /* no se debe entrar en una reunión sin plazo */
return FALSE;
}
/* La petición de parada quedó en pie para todos a la vez */
for (DWORD i = 0; i < count; i++) {
if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
CloseHandle(threads[i]); /* cerrar solo los que se pudieron reunir */
} else {
LogLastError(); /* WAIT_FAILED: identificador inválido, etc. */
ok = FALSE; /* no se informa de que «todos se detuvieron» */
}
}
return ok; /* si es FALSE no se debe pasar a liberar recursos compartidos */
}
La reunión: confirmar el éxito de la espera antes de seguir
StopWorkers comprueba primero el éxito de SetEvent. No se debe pasar a una reunión sin plazo si no se pudo entregar la petición de parada. De cada hilo se cierra solo el identificador para el que WaitForSingleObject devolvió WAIT_OBJECT_0; si la espera falla, no se informa de que «todos se detuvieron». El uso es: si el valor de retorno es FALSE, no se pasa a liberar recursos compartidos.
Que la espera de terminación sea de uno en uno se debe a que WaitForMultipleObjects tiene un tope de MAXIMUM_WAIT_OBJECTS (64) identificadores a la vez. En una matriz que lo supere la espera puede ser WAIT_FAILED. Si solo se espera a que terminen todos, un bucle que espera de uno en uno no tiene esa restricción de longitud de matriz.
flowchart TB
OWNER["Quien detiene"] -->|"SetEvent(hStopEvent)"| SE["Evento de detención<br/>(reinicio manual: visible para todos)"]
SE --> W1["Trabajador 1:<br/>espera a la vez parada y trabajo<br/>con WaitForMultipleObjects"]
SE --> W2["Trabajador 2:<br/>espera a la vez parada y trabajo<br/>con WaitForMultipleObjects"]
W1 --> C1["Limpia y hace return por sí mismo"]
W2 --> C2["Limpia y hace return por sí mismo"]
C1 --> J["Quien detiene espera los identificadores de hilo y reúne<br/>solo entonces se puede decir «se detuvo»"]
C2 --> J
Figura 4: El patrón del evento de detención. Si para detener se usa un evento de reinicio manual, un solo SetEvent despierta a la vez a todos los trabajadores en espera. Cada hilo decide por sí mismo cómo terminar, y se considera detenido al completar la reunión.
Respetar la configuración y el orden del evento de detención
El evento de detención se deja de reinicio manual, de modo que un SetEvent quede visible para todos los trabajadores. El papel no es el mismo que el aviso de trabajo.
Además, en la matriz de espera de WaitForMultipleObjects el evento de detención se pone el primero. En una espera como la de este ejemplo, con bWaitAll en FALSE, si varios objetos están señalizados se prioriza el identificador de índice menor. Por último, quien detiene cierra el identificador después de confirmar la reunión del identificador de hilo.
Un solo trabajador: despertar con un evento y vaciar la cola
La forma de arriba, «despertar con un evento de reinicio automático y vaciar toda la cola», está orientada a una configuración de un trabajador. Un evento de reinicio automático no cuenta el número de trabajos aunque se llame SetEvent varias veces. El estado señalizado es uno, así que avisos seguidos se fusionan.
Si se usa esa forma tal cual con varios trabajadores, puede despertarse solo uno y procesar en serie el trabajo acumulado.
Varios trabajadores: hacer corresponder un permiso de semáforo con un trabajo
Si varios trabajadores se reparten la cola, el aviso de trabajo se cambia a un semáforo. El lado que produce incrementa el permiso con ReleaseSemaphore(hSem, 1, NULL) cada vez que apila uno, y el lado que consume saca solo uno por cada espera con éxito. Como cada espera con éxito consume un permiso, se mantiene la correspondencia con el número de elementos.
En ese caso no se debe dejar el bucle de vaciado completo del ejemplo. Si se vacía la cola habiendo consumido un solo permiso, otro trabajador se despierta ante una cola vacía por los permisos que quedan, o el ReleaseSemaphore del lado que produce falla por superar el tope. El supuesto es «un permiso = un trabajo».
Aunque el procesamiento de un elemento sea largo, poder observar la detención
No basta con comprobar la detención entre elementos. Si dentro de ProcessNextItem hay una espera de bloqueo larga, también ahí se pasa el evento de detención y se espera a la vez, o se pone un tiempo de espera finito.
Si no, el apagado espera para siempre porque un elemento no termina. La misma idea que StopAsync de la edición .NET y jthread más join de la edición C++.
En la E/S de bloqueo, preparar también un camino de detención del lado de la E/S
Un hilo que espera en la E/S de una tubería, un socket o un puerto serie no puede, tal cual, volver a comprobar el evento de detención. Hace falta un camino que termine la espera también del lado de la E/S, incluida la espera superpuesta de OVERLAPPED y un evento, o la cancelación de la E/S con CancelIoEx.
Un ejemplo concreto se trata en «Trampas de las aplicaciones de comunicación serie».
7. DllMain y el bloqueo del cargador: el campo minado al escribir una DLL
Las piezas compartidas escritas en C suelen ser DLL, y ahí hay una restricción propia: el bloqueo del cargador. DllMain la llama el cargador del SO mientras retiene ese bloqueo, así que hacer lo siguiente dentro es causa de interbloqueo o de fallo.10
- Sincronizar con otros hilos (adquirir un bloqueo, esperar a que un hilo termine)
- Llamar a
LoadLibrary/FreeLibrary(da igual si es directa o indirecta) - Crear un hilo (peligroso si conlleva sincronización) o
ExitThread
Preparar una función de cierre explícita fuera de DllMain
«Al descargar la DLL, esperar en DllMain a que terminen los trabajadores» parece correcto a primera vista y es un interbloqueo típico. El hilo que termina también necesita el bloqueo del cargador para la entrega de DLL_THREAD_DETACH, y se esperan mutuamente con DllMain.10
Una DLL que tiene hilos publica funciones de inicialización y de cierre del estilo MyLib_Init / MyLib_Shutdown y arranca y reúne los hilos fuera de DllMain. Quien llama confirma la detención en la función de cierre y luego descarga. El propio DllMain se diseña, en lo posible, como un stub casi vacío.10
8. La opción de los hilos de C11: el estado actual
Si se quiere compartir código con otros SO sin depender de Win32, <threads.h> y <stdatomic.h> de C11 son una opción. La compatibilidad de MSVC a agosto de 2026, vista por separado, es la siguiente.11
| Función | Lugar en MSVC | Condiciones a comprobar |
|---|---|---|
<threads.h> (thrd_create / mtx_lock / cnd_wait) |
Admitido en Visual Studio 2022 17.8 | /std:c11 y el SDK de Windows correspondiente |
<stdatomic.h> |
experimental | Opción /experimental:c11atomics |
Lo importante es no pensar que los hilos y las operaciones atómicas están en la misma etapa de compatibilidad.
Si compartir código con Linux es un requisito, los hilos de C11 (o un envoltorio de pthread) tienen valor, pero en una base de código exclusiva de Windows el estilo Win32 de este artículo es más ventajoso por volumen de información, trayectoria y facilidad de depuración. Cualquiera que se elija, los principios de diseño hasta aquí (reducir el compartir, correspondencia de bloqueo y datos, parada cooperativa) no cambian.
9. Verificación y depuración: prepararse partiendo de que «no se reproduce»
Aunque las pruebas habituales tengan éxito, no se puede afirmar que no hay contención. Queda la posibilidad de que «esta vez no se dio el orden de ejecución problemático». Se prepara en tres capas: confirmar el diseño, poder observar la anomalía, agitar el orden de ejecución con carga.
Revisión de diseño: hacer corresponder datos, bloqueos y camino de detención
Se comprueba en una tabla los datos mutables que se comparten, el bloqueo que los protege, el orden de adquisición de varios bloqueos y los trabajadores a los que llega el evento de detención. Es el trabajo de ver si la disciplina del apartado 5.1 corresponde al código real.
Si esa correspondencia no se puede escribir, «funciona» no sirve como fundamento de que el diseño está terminado.
Observación: dejar el lugar de la espera con tiempo de espera, registros y volcado
En lugar de dejar todas las esperas en un INFINITE incondicional, se pone tiempo de espera en los puntos clave y se deja el vencimiento en el registro. Es para tratar como un fallo investigable un estado que, de otro modo, solo seguía esperando. Ahora bien, que se agote el tiempo no es la confirmación de que el hilo terminó, y no por eso se pueden liberar los recursos compartidos.
En un cuelgue se toma un volcado, se miran las pilas de todos los hilos y se comprueba que las esperas de bloqueo no formen un ciclo. En errores alrededor de DLL también se usa Application Verifier, recomendado de forma oficial.10 La preparación de registros y volcados se consulta en «Diseño para dejar registro y volcado cuando se cae una aplicación Windows».
Prueba de carga: probar órdenes de ejecución que apenas aparecen en la máquina de desarrollo
Una prueba de estrés —correr largo tiempo con más hilos que núcleos, aleatorizar el orden de procesamiento, insertar retardos artificiales— aumenta las ocasiones de que aparezca el fallo. También se comprueba la combinación de una compilación de publicación optimizada y una carga alta.
La prueba de carga no sustituye la revisión de diseño. Se combinan las tres capas para acercarse a un estado en el que, al reproducirse, se pueda seguir la causa.
10. Resumen: lista de verificación de la edición C
- ¿Toda creación de hilos es
_beginthreadex(no se mezclanCreateThread/_beginthread)? - ¿El identificador de hilo se cierra con
CloseHandledespués de reunir (WaitForSingleObject)? - ¿No se fabrican hilos propios para trabajo de vida corta (se puede lanzar a la API del grupo de subprocesos)?
- ¿La exclusión dentro del proceso es un bloqueo SRW / CRITICAL_SECTION (no se usa mal un Mutex)?
- ¿Los contadores y banderas compartidos son de la familia Interlocked, no a merced de
volatile? - ¿No queda sondeo con
Sleep(se sustituyó por variable de condición o espera de evento)? - ¿No hay ni un
TerminateThread(terminación forzada de otro hilo)? ¿El trabajador termina conreturndesde la función de hilo, no llamando aExitThread(la limpieza del CRT corre correctamente vía_endthreadex)? - ¿Hay camino de detención con evento de detención +
WaitForMultipleObjectsen todos los trabajadores, y se puede despertar también un hilo en E/S de bloqueo? - ¿La liberación de bloqueos e identificadores está garantizada en todos los caminos de retorno (disciplina de
goto cleanup)? - ¿DllMain no crea hilos, no sincroniza y no espera a que terminen?
A cambio de no tener el apoyo del lenguaje, en el multithreading de C la elección de la API y la disciplina son tal cual la calidad. Si se toma como valor por defecto este conjunto de cuatro —_beginthreadex, bloqueo SRW, Interlocked, evento de detención—, también en C se puede diseñar a distancia de «de vez en cuando se queda colgado».
Artículos relacionados
- Buenas prácticas de multithreading en la práctica: edición .NET
- Buenas prácticas de multithreading en la práctica: edición C++
- Buenas prácticas de multithreading en la práctica: edición Java
- Trampas de la memoria compartida y buenas prácticas en la práctica
- Por qué en Windows conviene priorizar la espera de un evento frente a Sleep(1)
- Trampas de las aplicaciones de comunicación serie
- Diseño para dejar registro y volcado cuando se cae una aplicación Windows
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa de la revisión del diseño de multithreading para procesos residentes, aplicaciones de control de equipos y DLL escritos en C, de la investigación de la causa de cuelgues y fallos originados en TerminateThread o en fugas de bloqueo (análisis de volcado), y de la consultoría técnica para añadir hilos a código C heredado.
- Consultoría técnica y revisión de diseño
- Investigación de fallos y análisis de causas
- Desarrollo de aplicaciones Windows
- Contacto
Referencias
-
Microsoft Learn, CreateThread function. Sobre que los hilos de un ejecutable que llama al CRT deben gestionarse con _beginthreadex / _endthreadex y no con CreateThread / ExitThread; y que si un hilo creado con CreateThread llama al CRT, en baja memoria el CRT puede terminar el proceso. ↩ ↩2
-
Microsoft Learn, Multithreading with C and Win32. Sobre que en un programa que llama a la biblioteca CRT los hilos deben iniciarse con _beginthread / _beginthreadex y no con CreateThread / ExitThread de Win32; que la familia _beginthread inicializa las variables por hilo del CRT; y que SuspendThread puede detener un hilo a mitad de un acceso a las estructuras internas del CRT y provocar un interbloqueo. ↩ ↩2
-
Microsoft Learn, CreateThreadpoolWork function. Sobre que se crea un objeto de trabajo con CreateThreadpoolWork y, cada vez que se llama SubmitThreadpoolWork, un hilo trabajador del grupo ejecuta la devolución de llamada; que el entorno de ejecución se puede indicar con un entorno de devolución de llamada (TP_CALLBACK_ENVIRON); y que está disponible desde Windows Vista. ↩ ↩2
-
Microsoft Learn, Thread Pools. Sobre que el grupo de subprocesos conviene a aplicaciones que ejecutan en masa trabajo corto de forma asíncrona o que crean con frecuencia hilos de vida corta; los elementos de la API del grupo rediseñada en Vista; y, como buenas prácticas, no terminar un hilo del grupo con TerminateThread ni llamar ExitThread desde la devolución de llamada, limpiar antes de volver el estado creado en la devolución de llamada, y mantener vivos los identificadores de espera hasta que el grupo deje de usarlos. ↩ ↩2
-
Microsoft Learn, About Synchronization. Sobre la pauta de elección de los primitivos de sincronización de Win32: el bloqueo SRW es el valor por defecto en código nuevo, del tamaño de un puntero y suele terminar en modo usuario; CRITICAL_SECTION se usa cuando hace falta adquisición recursiva; Mutex es siempre un objeto del kernel y se usa para sincronización con nombre entre procesos y en combinación con WaitForMultipleObjects; usar Mutex para la sincronización dentro del proceso es «un error común» que en operaciones frecuentes resulta mucho más lento; el semáforo se usa para limitar el número de accesos simultáneos a un grupo de recursos y el evento para notificar. ↩ ↩2 ↩3
-
Microsoft Learn, Interlocked Variable Access. Sobre que las funciones Interlocked sincronizan el acceso a una variable compartida entre varios hilos y hacen la operación indivisible; que InterlockedIncrement / Decrement agrupan lectura, suma y escritura de vuelta en una sola operación atómica y, sin sincronización, dos incrementos simultáneos pueden perderse en uno; la familia InterlockedExchange / InterlockedCompareExchange; que si la variable está en memoria compartida también sirve entre hilos de procesos distintos; y que la mayoría de las funciones Interlocked ofrecen una barrera de memoria completa y se puede elegir la semántica de orden con las versiones Acquire / Release. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using Condition Variables. Sobre el ejemplo de implementación de una cola productor/consumidor con un búfer circular acotado protegido por CRITICAL_SECTION; la estructura de crear la variable de condición con InitializeConditionVariable, esperar el consumidor con SleepConditionVariableCS y despertar a la otra parte con WakeConditionVariable; y que las variables de condición se admiten desde Windows Vista. ↩ ↩2 ↩3
-
Microsoft Learn, TerminateThread function. Sobre que TerminateThread termina el hilo objetivo sin dejarle ejecutar ningún código en modo usuario; que si el objetivo retiene una sección crítica no se libera; que si está reservando memoria del montón no se libera el bloqueo del montón; que se puede destruir el estado de kernel32 y el estado global de las DLL; y que es «una función peligrosa que solo debe usarse en los casos más extremos» y no debe llamarse salvo cuando se conoce y se controla por completo el código que el hilo objetivo puede ejecutar. ↩ ↩2 ↩3
-
Microsoft Learn, Warning C6258. Sobre que la advertencia de análisis de código C6258 detecta el uso de TerminateThread; que con TerminateThread no se puede hacer una limpieza adecuada del hilo; y que como forma correcta de terminar se muestra el procedimiento de crear un evento con CreateEvent, que cada hilo vigile el estado del evento con WaitForSingleObject y, al quedar señalizado, termine la ejecución por sí mismo. ↩ ↩2 ↩3
-
Microsoft Learn, Dynamic-Link Library Best Practices. Sobre que DllMain se llama mientras se retiene el bloqueo del cargador y las API que se pueden llamar tienen restricciones graves; que sincronizar con otros hilos dentro de DllMain provoca interbloqueo; que llamar a LoadLibrary está prohibido; el esquema de interbloqueo al esperar la terminación de un hilo dentro de DllMain al descargar la DLL, porque el hilo que termina y la entrega de DLL_THREAD_DETACH se esperan mutuamente; que el DllMain ideal es un stub casi vacío y la inicialización debe diferirse en lo posible; y que hay que definir una jerarquía de bloqueos y poner el bloqueo del cargador en lo más alto. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. Sobre la tabla de compatibilidad de la biblioteca estándar de C: que los hilos de C11 (threads.h) se admiten desde Visual Studio 2022 17.8; que stdatomic.h se trata como experimental (opción /experimental:c11atomics); y que la compatibilidad del compilador con C11 / C17 requiere Visual Studio 2019 16.8 o posterior y el SDK de Windows correspondiente. ↩ ↩2
-
Microsoft Learn, Interlocked Variable Access. Sobre que la lectura o escritura simple de una variable de 32 bits correctamente alineada es atómica, pero no se garantiza la sincronización (el orden) del acceso; que la lectura o escritura simple de una variable de 64 bits es atómica en Windows de 64 bits pero no está garantizada en Windows de 32 bits; y que para variables de otros tamaños no se garantiza la atomicidad en ninguna plataforma. ↩ ↩2
-
Microsoft Learn, _beginthread, _beginthreadex. Sobre por qué _beginthreadex es más seguro que _beginthread: si un hilo creado con _beginthread termina pronto, el identificador ya devuelto puede quedar inválido y apuntar a otro hilo; el identificador de _beginthreadex lo cierra quien llamó con CloseHandle y su validez está garantizada; con _beginthreadex se puede pasar el identificador a las API de sincronización; la función de hilo usa el convenio __stdcall y devuelve el código de terminación del hilo; y hace falta enlazar con la versión de subprocesos del CRT. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Buenas prácticas de multithreading en la práctica: edición C++ — eliminar los fallos desde la estructura con RAII y jthread
En C++ una condición de carrera de datos es comportamiento indefinido. Se cubren la trampa del destructor de std::thread, la parada con j...
Buenas prácticas de multithreading en la práctica: edición .NET — qué decidir antes de aumentar los hilos
Evitar que los hilos de .NET/C# se caigan o se cuelguen: apoyarse en Task, reducir el estado mutable compartido, disciplina de bloqueos, ...
Aplicaciones que se rompen al reanudar de la suspensión — Eventos de energía y aplicaciones empresariales que sobreviven a la reanudación
Abre el portátil y las conexiones de la aplicación empresarial están cortadas: la causa es un diseño que no contempló la suspensión. Este...
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...
Qué es realmente «No responde» — Cómo decide Windows que una aplicación se ha colgado, y cómo diseñar aplicaciones que no lo hagan
«No responde» de Windows es un mecanismo en el que el sistema operativo juzga que una ventana no ha recuperado un mensaje durante 5 segun...
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.
- ¿Debo usar CreateThread o _beginthreadex?
- Si el hilo va a llamar funciones de la biblioteca en tiempo de ejecución de C (CRT), use _beginthreadex. _beginthreadex inicializa los datos internos que el CRT necesita por hilo antes de iniciarlo. La documentación oficial indica explícitamente que, si un hilo creado con CreateThread llama funciones del CRT, este puede terminar el proceso en situaciones de memoria insuficiente. En la práctica, un hilo de una aplicación escrita en C casi siempre acaba llamando alguna función del CRT (printf, malloc, strtok, etc.), así que puede recordarlo simplemente como «siempre _beginthreadex». Además, _beginthread (sin «ex») tiene la trampa de que, si el hilo termina pronto, el identificador devuelto queda inválido, por lo que también en ese caso conviene elegir _beginthreadex.
- ¿No se debe detener un hilo con TerminateThread?
- No se debe. TerminateThread elimina el hilo objetivo sin dejarle ejecutar absolutamente ningún código en modo usuario, de modo que si ese hilo mantenía una sección crítica, esta nunca se libera; si estaba reservando memoria del montón, el bloqueo del montón queda retenido; y si estaba manipulando el estado global de una DLL, ese estado queda dañado. La documentación oficial la describe explícitamente como «una función peligrosa que solo debe usarse en los casos más extremos», y el análisis de código la detecta como la advertencia C6258. La forma correcta de detener es una parada cooperativa: se crea un evento de detención y cada hilo lo vigila con WaitForSingleObject / WaitForMultipleObjects, se limpia a sí mismo y termina por su cuenta.
- Usaba Mutex para la exclusión dentro de un mismo proceso. ¿Qué tiene de malo?
- Funciona, pero perjudica mucho el rendimiento. El Mutex de Win32 es siempre un objeto del kernel, por lo que cada adquisición y liberación implica una transición a modo kernel. Para la exclusión dentro de un mismo proceso, un bloqueo SRW o una CRITICAL_SECTION —que se resuelven en modo usuario y solo caen a la espera del kernel en caso de contención— son mucho más rápidos, y la documentación oficial señala explícitamente que usar Mutex para la sincronización dentro de un proceso es «un error común». El Mutex tiene su lugar cuando se necesita exclusión entre procesos mediante un objeto con nombre, o cuando se quiere esperar simultáneamente junto con otros objetos del kernel usando WaitForMultipleObjects.
- ¿Se pueden usar threads.h y stdatomic.h de C11 en Windows?
- En MSVC, los hilos de C11 (threads.h) se admiten desde Visual Studio 2022 17.8 (hace falta /std:c11 y el SDK de Windows correspondiente). Por otro lado, stdatomic.h se trata como experimental y está en la etapa de exigir la opción /experimental:c11atomics (según la tabla de compatibilidad oficial a agosto de 2026). Es una opción cuando la portabilidad es la máxima prioridad, pero si la base de código es exclusiva de Windows, resulta más realista escribirla con la API Win32 (_beginthreadex, bloqueos SRW, variables de condición, Interlocked), por trayectoria y volumen de información.
- ¿Basta con añadir volatile a una bandera compartida para que sea segura?
- No. El volatile de C solo impide ciertas optimizaciones del compilador (como guardar el valor en un registro); no garantiza ni la atomicidad de la operación ni el orden de memoria entre procesadores. La lectura o escritura simple de una variable de 32 bits correctamente alineada sí es atómica de por sí en Windows, pero «leer, sumar y volver a escribir» se descompone, y tampoco queda definido el orden respecto a las operaciones de memoria de alrededor. Para actualizar contadores o banderas compartidas, use las funciones de la familia Interlocked. La mayoría de las funciones Interlocked llevan una barrera de memoria completa, de modo que la garantía de orden se obtiene al mismo tiempo. Cuando hay que proteger varias variables a la vez, use un bloqueo SRW o una CRITICAL_SECTION.
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.