API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork

· Actualizado el: · · Windows, Multithreading, C++, Desarrollo en Windows, Win32 API, Mejora del rendimiento

Historial de revisiones (primera versión, publicada el 22 Aug 2026)
Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22176800)

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). API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork. KomuraSoft LLC. https://comcomponent.com/es/blog/win32-thread-pool-api/

DOI (archivo registrado)
10.5281/zenodo.22176800
DOI (última versión registrada)
10.5281/zenodo.22176801

«Un CreateThread por cliente.» «Uno para el temporizador.» «Uno para esperar un evento.» En el código nativo de Windows se tiende a multiplicar hilos para trabajos pequeños. Cada hilo consume una pila y un objeto del kernel, y crearlos y destruirlos también tiene un coste.

Lo que absorbe ese desajuste es la API del grupo de hilos de Win32, estándar del sistema operativo. La aplicación entrega el procesamiento que quiere ejecutar como devolución de llamada y deja la gestión de los hilos trabajadores al sistema operativo. «No crear hilos» no significa que los hilos dejen de ser necesarios. Significa que la aplicación no crea y destruye uno por cada trabajo.1

Este artículo se dirige a quienes escriben aplicaciones, servicios y DLL de Windows en C/C++, y sigue el orden elección de la herramienta → selección del objeto → implementación de work → precauciones de las devoluciones de llamada y del apagado. El tema es la familia CreateThreadpoolWork rediseñada en Windows Vista. El código es un extracto que muestra el esqueleto del uso; las partes propias de la aplicación, como la cola, se omiten.

1. Primero la conclusión

Si procesa un gran número de trabajos de corta duración, gestione trabajos en lugar de hilos. Quién diseña cuándo se detiene un trabajo y cuándo puede liberarse, sin embargo, es la aplicación.

La decisión gira en torno a tres ejes.

  • Elegir la herramienta. Si bastan las herramientas de C++ estándar o de .NET, úselas. El turno de esta API llega cuando quiere unificar temporizadores, esperas y finalización de E/S en Win32, o dividir grupos.
  • Entregar el trabajo en unidades de trabajos. Use work, timer, wait e io de la API nueva, y deje en un hilo dedicado los trabajos que necesitan estado propio del hilo, como una prioridad de hilo o una STA de COM.
  • Construir el apagado como parte del conjunto. La forma básica es «dejar de enviar → esperar la finalización → cerrar». Dentro de una devolución de llamada, no bloquee durante mucho tiempo, no espere de forma síncrona trabajo del mismo grupo y restablezca el estado del hilo.

Si quiere poner algo en marcha primero, empiece por el ejemplo de work del capítulo 4 y luego compruebe la disciplina de las devoluciones de llamada en el capítulo 5. El apagado agrupado de varios objetos se trata en el capítulo 6, y la descarga de una DLL en el capítulo 7.

2. Elección — grupo, hilo dedicado y biblioteca estándar

2.1 Un grupo encaja con trabajo corto, numeroso y centrado en la espera

Un grupo de hilos es un conjunto de hilos trabajadores gestionados por el sistema operativo. Los trabajadores ejecutan las devoluciones de llamada una tras otra, y el sistema operativo ajusta su número según la carga. Al entregar el trabajo que hacía creando y desmontando hilos propios, reduce tanto el código de gestión como el coste de creación y destrucción.1

La documentación oficial enumera como candidatas las aplicaciones que emiten en paralelo un gran número de elementos de trabajo pequeños, las que crean y destruyen con frecuencia hilos de corta duración, las que procesan en paralelo trabajo independiente en segundo plano, y las que tienen hilos dedicados a esperar objetos del kernel o eventos. La búsqueda, las E/S de red y consolidar los hilos que solo esperan son típicos.1

Por otro lado, el trabajo que necesita un cambio de prioridad de hilo, una STA de COM, o que sigue ejecutándose durante toda la vida del proceso, se queda en un hilo dedicado. El criterio es si basta con que el procesamiento se ejecute, o si el propio hilo necesita una «personalidad».

Elección entre hilo dedicado y grupoComprobar primero si el hilo necesita una personalidad como una prioridad o una STA, y si se ejecuta durante mucho tiempo; solo el trabajo corto, numeroso o de espera que no encaja en ninguno de los dos va al grupo de hilosSíNoSíNo¿Necesita personalidad, p. ej. prioridad o STA?Mantener en un hilo dedicado¿Se ejecuta durante mucho tiempo?Poner en el grupo de hilosTrabajo corto, esperas, temporizadores, finalizaciones de E/S

Figura 1: Al grupo solo pertenece el trabajo que «no necesita personalidad y termina pronto». Todo lo demás se queda en un hilo dedicado como hasta ahora.

2.2 Preguntar primero si basta C++ estándar o .NET

Si la granularidad la cubren std::async o std::thread de C++, la biblioteca estándar es la primera candidata. Es portable, y el comportamiento de std::async y future se puede tratar con base en el estándar.2

Las razones para usar de forma directa el grupo de hilos de Win32 son requisitos como querer un mecanismo unificado de devolución de llamada que incluya timer, wait e io; querer grupos o números de hilos distintos por tipo de trabajo; no querer mantener hilos propios dentro de una DLL o un componente COM. Separe la pregunta de si simplemente quiere paralelismo de la de si necesita control específico de Windows.

Decidir con las herramientas de qué capa escribirSi async o thread de C++ estándar bastan, usarlos; usar de forma directa el grupo de hilos de Win32 cuando hace falta unificar temporizadores, esperas y finalización de E/S, dividir grupos o controlar el número de hilos, o evitar hilos propios dentro de una DLL o un componente COMSíNoUnificar timer, wait e ioDividir grupos / controlar númerosEvitar hilos propios en una DLL¿Bastan las herramientas de C++ estándar?std::async / std::thread¿Qué se necesita?Grupo de hilos de Win32

Figura 2: En caso de duda, empiece por la biblioteca estándar; el turno de esta API llega cuando aparece un requisito que ella no puede expresar.

En .NET, ThreadPool y Task desempeñan el mismo papel, y la finalización de E/S está ligada a IOCP. Para una explicación detallada de la relación, consulte el artículo sobre IOCP y el grupo de hilos de .NET. No hace falta bajar a la API de Win32 donde bastan las herramientas de la capa de encima.

2.3 En código nuevo, use la API nueva a partir de Vista

Hay dos generaciones de API del grupo de hilos de Win32: la API antigua que continúa desde Windows 2000, como QueueUserWorkItem y RegisterWaitForSingleObject, y la familia CreateThreadpoolWork rediseñada por completo en Windows Vista.

La API nueva unificó los tipos de hilo trabajador y aportó una sola cola de temporizador, hilos persistentes dedicados, varios grupos independientes en un proceso y grupos de limpieza. La documentación oficial cita también como ventajas la sencillez, la fiabilidad, el rendimiento y la flexibilidad.13

La API antigua tiene además una restricción estructural: no hay forma de cancelar un trabajo una vez encolado. Use la API nueva en código nuevo y, al revisar código existente, parta de la correspondencia siguiente.3

Correspondencia entre la API antigua del grupo de hilos y la API nuevaQueueUserWorkItem de la API antigua se sustituye por el objeto work de la API nueva, las colas de temporizador por timer, las esperas registradas por wait y BindIoCompletionCallback por ioQueueUserWorkItemworkColas de temporizadortimerEsperas registradaswaitBindIoCompletionCallbackio

Figura 3: El destino de migración desde la API antigua está fijado uno a uno. Un inventario del código existente puede empezar por esta correspondencia.

3. Funcionamiento — cuatro objetos con distintas condiciones de disparo

3.1 Elegir según qué debe disparar la devolución de llamada

En el centro de la API nueva están los cuatro tipos siguientes. Elija no solo por «qué procesar», sino por qué debe disparar la devolución de llamada.4

Objeto Función de creación Condición de disparo de la devolución de llamada
work CreateThreadpoolWork Cuando se envía con SubmitThreadpoolWork
timer CreateThreadpoolTimer Cuando llega el instante o el periodo indicados
wait CreateThreadpoolWait Cuando un objeto del kernel pasa a señalizado
io CreateThreadpoolIo Cuando finaliza la E/S asíncrona del identificador asociado

Las condiciones de disparo difieren, pero quienes ejecutan son los trabajadores del mismo grupo. En lugar de escribir el procesamiento periódico, la reacción a eventos y el tratamiento de finalización de E/S cada uno en su hilo dedicado, puede alinearlos en un mecanismo común de devolución de llamada.

Separar las condiciones de disparo de la ejecución de las devoluciones de llamadaLos trabajadores del grupo ejecutan las devoluciones de llamada que objetos con condiciones de disparo han dejado listas, y un trabajador que ha terminado se usa también para la siguiente devolución de llamada, de modo que la aplicación no crea un hilo por trabajoLa aplicación indica el trabajo y su condición de disparoEl objeto espera la condiciónLa devolución de llamada queda lista para ejecutarseLos trabajadores del grupo la ejecutanTerminar y devolver el trabajadorReutilizado también para la siguiente devolución de llamada

Figura 4: Separar el mecanismo que espera el disparo de los trabajadores que ejecutan el procesamiento reduce la gestión de hilos por trabajo.

3.2 También se pueden sustituir los «hilos que solo duermen y esperan»

El beneficio del grupo no se limita al trabajo que usa la CPU. Los temporizadores se consolidan en una sola cola de temporizador, y la espera de varios identificadores en un número reducido de hilos de espera.1

Por ejemplo, si tiene cinco hilos que solo duermen para «ejecutarse cuando el evento se señaliza», puede pensar en sustituirlos por cinco objetos wait. En lugar de mantener un hilo que duerme por su cuenta para cada uno, ejecuta el procesamiento al señalizarse como devolución de llamada.

Sustituir hilos que solo esperan por objetos waitLos hilos que solo esperan, uno por evento, se consolidan en los hilos de espera del grupo al sustituirlos por objetos wait, y la devolución de llamada se ejecuta solo al señalizarse5 hilos que solo esperan, durmiendo por separadoConsume 5 pilas y 5 hilos5 objetos waitConsolidados en los hilos de espera del grupoDevolución de llamada solo al señalizarse

Figura 5: Los «hilos que solo duermen y esperan» se pueden eliminar convirtiéndolos en objetos wait. Es el primer paso evidente de una migración al grupo.

4. Bases de la implementación — crear un work, enviarlo y apagar con seguridad

4.1 Primero, un recorrido de la creación al apagado

Recorra el uso una vez con el objeto más básico, work. CreateThreadpoolWork vincula la devolución de llamada a un context, y SubmitThreadpoolWork solicita la ejecución. Si el tercer argumento es NULL, se usa el grupo predeterminado del proceso. Muchos usos bastan con este grupo predeterminado.56

Lo siguiente es un extracto que muestra el procedimiento, no un programa completo que compile tal cual. WORK_QUEUE, ITEM, Enqueue, Dequeue y ProcessItem representan el procesamiento del lado de la aplicación. Implemente por separado la exclusión mutua de la cola, detener el lado que envía y el tratamiento de fallos, y si falla la creación del work, no continúe con el envío, la espera y la liberación posteriores.

VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
                           PVOID context, PTP_WORK work)
{
    // El context se fija en la creación. Los datos de cada elemento se pasan por una cola sincronizada
    WORK_QUEUE* queue = (WORK_QUEUE*)context;
    ITEM* item = Dequeue(queue);        // Extraer un elemento con exclusión mutua
    ProcessItem(item);
}

// 1) Crear (vincular la devolución de llamada al contexto compartido, es decir, la cola)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* tratamiento del fallo con GetLastError */ }

// 2) Enviar una vez por cada elemento encolado (hacer coincidir el número de elementos y de envíos)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);

// 3) Detener el lado que envía y luego esperar la finalización (TRUE intenta también cancelar lo que aún no ha empezado)
WaitForThreadpoolWorkCallbacks(work, FALSE);

// 4) Cerrar
CloseThreadpoolWork(work);
Ciclo de vida de un objeto workCrear con CreateThreadpoolWork; enviar con SubmitThreadpoolWork ejecuta las devoluciones de llamada en paralelo. En el apagado, detener primero los envíos nuevos, esperar la finalización de todas las devoluciones de llamada con WaitForThreadpoolWorkCallbacks y luego cerrar con CloseThreadpoolWorkCrear con CreateThreadpoolWorkEnviar con SubmitThreadpoolWork (repetible)Las devoluciones de llamada se ejecutan en paraleloDetener los envíos nuevosEsperar la finalización con WaitForThreadpoolWorkCallbacksCerrar con CloseThreadpoolWork

Figura 6: El orden de apagado es «dejar de enviar → esperar la finalización → cerrar». Si se salta algún paso, hay acceso después de liberar o una condición de carrera.

4.2 Un work se puede reutilizar, pero el context no cambia en cada envío

El mismo objeto work se puede enviar varias veces, incluso antes de que termine la devolución de llamada anterior. Cada envío ejecuta la devolución de llamada, y varias instancias se ejecutan en paralelo. El número de hilos que se usan de hecho puede ajustarlo el grupo por eficiencia.7

Lo que hay que distinguir aquí es el work y los datos de cada elemento de trabajo. El context que se pasa a la devolución de llamada se fija en la creación. Para procesar N elementos de datos distintos con un work, haga de una cola sincronizada el context, como en el ejemplo anterior. Envíe una vez por cada elemento encolado y haga que la devolución de llamada tome un elemento de la cola. Un diseño que crea un objeto work por elemento también es válido.5

Recibir los datos de cada elemento a través de un context fijoEl context se fija a una cola compartida al crear el work; el lado que envía envía una vez por cada elemento encolado, y cada devolución de llamada que se ejecuta en paralelo toma de la cola un elemento cada vez con exclusión mutuaFijar el context al crear el workCola compartida con exclusión mutuaEncolar un elementoEnviar una vez por elementoLas devoluciones de llamada se ejecutan en paraleloExtraer un elemento cada unaProcesar el elemento extraído

Figura 7: Lo que queda fijo es el context que apunta a la cola; los datos de cada elemento se pasan por la cola sincronizada.

4.3 En el apagado, deje de enviar antes de esperar

El orden de apagado seguro es «detener los envíos nuevos → esperar la finalización con WaitForThreadpoolWorkCallbacks → cerrar con CloseThreadpoolWork». La memoria a la que se refiere la devolución de llamada tampoco debe liberarse antes de confirmar la finalización. Si libera el destino de la referencia mientras queda procesamiento en ejecución o pendiente, se produce un acceso después de liberar.6

Pensar «llamé a la espera de finalización, así que es seguro» no basta. Si otro hilo todavía puede llamar a SubmitThreadpoolWork, un envío posterior a la espera entra en carrera con Close. Para que la espera sea un hito del apagado, detener antes el lado que envía es un requisito previo.

Si el segundo argumento de WaitForThreadpoolWorkCallbacks es FALSE, espera la finalización; si es TRUE, también solicita la cancelación de las devoluciones de llamada que aún no han empezado. Eso no significa que se pueda abandonar el procesamiento que ya está en ejecución. Cómo apagar varios objetos juntos se trata en el capítulo 6.

5. Disciplina de las devoluciones de llamada — no monopolizar ni contaminar un hilo prestado

5.1 Declare el trabajo largo y compruebe también el valor de retorno

El grupo ajusta su número de hilos asumiendo que las devoluciones de llamada regresan pronto. Si continúa un procesamiento largo o una espera larga sin indicarlo, se retrasa la ejecución de otras devoluciones de llamada. Cuando el trabajo puede tardar, indíqueselo al grupo con CallbackMayRunLong o muévalo a un hilo dedicado.8

Sin embargo, llamar a CallbackMayRunLong no basta. La función devuelve FALSE cuando no puede poner un trabajador a disposición de otras devoluciones de llamada. En lugar de ignorar el valor de retorno y seguir bloqueando, en ese caso incline a no atascar el grupo: dividir el procesamiento, moverlo a un hilo dedicado, etc.8

Decidir cómo tratar una devolución de llamada de larga duraciónEl trabajo que necesita un procesamiento largo o una espera larga se mueve a un hilo dedicado o se anuncia al grupo con CallbackMayRunLong; si vuelve FALSE porque no se pudo disponer de otro trabajador, se evita bloquear dividiendo o moviendo a un hilo dedicadoDedicarloEn el grupoTRUEFALSEHace falta un procesamiento o una espera largosDónde ejecutarloMover a un hilo dedicadoNotificar con CallbackMayRunLong¿Se obtuvo otro trabajador?Ejecutar el procesamiento largoDividirlo o moverlo a uno dedicado

Figura 8: Anunciar un procesamiento largo es un conjunto que incluye comprobar el valor de retorno y decidir la acción siguiente.

5.2 No espere de forma síncrona, desde un trabajador, trabajo del mismo grupo

Un diseño en el que la devolución de llamada A envía el trabajo B al mismo grupo y espera su finalización con WaitForThreadpoolWorkCallbacks o similar requiere cuidado. Cuando todos los trabajadores están «esperando el trabajo de otro trabajador», no queda ningún trabajador libre para ejecutar B, y se produce un interbloqueo por hambruna del grupo.

El remedio no es reservar un trabajador para esperar, sino pasar a una forma de continuación en la que la devolución de llamada de finalización de B envía el trabajo siguiente. No escriba las dependencias como esperas que ocupan un trabajador.

Estructura de un interbloqueo por hambruna del grupoSi todos los hilos trabajadores esperan de forma síncrona la finalización de otro trabajo enviado al mismo grupo, no existe un trabajador libre que pueda ejecutarlo, y todos esperan para siempre en un interbloqueoTrabajador 1: esperando a que termine el trabajo XTrabajos X e Y esperando a ejecutarseTrabajador 2: esperando a que termine el trabajo YNo hay trabajador libre que los ejecuteTodos esperan para siempre (interbloqueo por hambruna)

Figura 9: Si un trabajador espera de forma síncrona a un trabajador, no queda nadie que ejecute el trabajo esperado.

5.3 Restablezca el estado del hilo antes de volver

Un hilo trabajador se usa también para la siguiente devolución de llamada, sin relación. Dejar la prioridad cambiada, dejar estado de inicialización COM, dejar un valor en TLS u olvidar liberar un bloqueo se arrastra al trabajo siguiente. No debe asumir que la función que envía al grupo, ni el procesamiento que llama, se ejecuta en un hilo dedicado.9

También hay API que ligan la limpieza al final de la devolución de llamada. Por ejemplo, LeaveCriticalSectionWhenCallbackReturns pide al grupo que libere la sección crítica después de que la devolución de llamada regrese.4

No arrastrar el estado del hilo a la siguiente devolución de llamadaComo otra devolución de llamada reutiliza el mismo trabajador, volver dejando estado de prioridad, COM, TLS o bloqueo contamina el trabajo siguiente; limpiar y restablecer el estado evita ese arrastreNoSíLa devolución de llamada A toma prestado un trabajador¿Se limpió antes de volver?Reutilizado con el estado dejadoAfecta a B, sin relaciónTrabajador devuelto en su estado originalLa siguiente devolución de llamada B lo usa

Figura 10: El hilo está prestado, así que la responsabilidad no es solo el resultado del procesamiento, sino el estado del hilo al devolverlo.

5.4 No deje que las excepciones no controladas se escapen del trabajador

Una excepción no controlada en un hilo trabajador puede arrastrar todo el proceso. Igual que con la función de hilo de un hilo dedicado, aplique la política de capturar las excepciones en la entrada de la devolución de llamada y registrarlas. Dejar la ejecución al grupo no elimina la necesidad de tratar los fallos ocurridos dentro del trabajo.

6. Separar la configuración — grupos personalizados y grupos de limpieza

6.1 Use un grupo personalizado para aislar tipos de trabajo

Cuando el grupo predeterminado ya no basta, puede crear un grupo independiente con CreateThreadpool. SetThreadpoolThreadMaximum y SetThreadpoolThreadMinimum fijan el tope y el suelo del número de hilos trabajadores.10

El propósito típico es el aislamiento. Para que «un procesamiento por lotes que puede ser lento» no agote los trabajadores de «un trabajo que debe responder de inmediato», divide los grupos y da a cada uno un presupuesto de hilos. No se trata simplemente de añadir hilos; se trata de separar qué trabajo usa qué trabajadores.

6.2 Vincule el destino de ejecución y la limpieza con un entorno de devolución de llamada

Lo que indica en qué grupo ejecutarse es TP_CALLBACK_ENVIRON. Inicializa este entorno de devolución de llamada, indica el grupo con SetThreadpoolCallbackPool y lo pasa a CreateThreadpoolWork y similares. El tercer argumento, que era NULL en el ejemplo del capítulo 4, es el lugar del entorno.5

A través del mismo entorno también se puede adjuntar un grupo de limpieza. El grupo personalizado es el destino de ejecución; el grupo de limpieza es la unidad de limpieza. Ver estos dos papeles por separado facilita seguir la configuración.

Vincular la configuración mediante un entorno de devolución de llamadaEl entorno de devolución de llamada apunta a un grupo personalizado y a un grupo de limpieza; los objetos work y timer creados con ese entorno se ejecutan en ese grupo, y la operación masiva del grupo de limpieza reúne la espera de finalización y la liberaciónEntorno de devolución de llamada (TP_CALLBACK_ENVIRON)Grupo personalizado (controla el número de hilos)Grupo de limpiezaSe pasa al crear work / timer / wait / ioEsperar la finalización y liberar en bloque

Figura 11: Un entorno de devolución de llamada es el mecanismo que inyecta en la creación del objeto «en qué grupo se ejecuta y quién limpia».

6.3 Reúna la espera de finalización y la liberación de varios objetos

Cuando work, timer y otros objetos se multiplican dentro de un módulo, el apagado se convierte en una lista de «esperar cada uno, cerrar cada uno». Si crea un grupo con CreateThreadpoolCleanupGroup y hace miembros a los objetos creados a través del entorno de devolución de llamada, un solo CloseThreadpoolCleanupGroupMembers reúne la espera de finalización y la liberación de todos los objetos miembros.46

También aquí el objetivo es no dejar atrás una devolución de llamada en ejecución. Elija entre apagar los work de uno en uno, como en el capítulo 4, y apagar a nivel de grupo, según el número de objetos que gestione.

7. Uso desde una DLL — no descargue antes que las devoluciones de llamada

7.1 Espere la finalización en una función de apagado explícita, no en DllMain

Lo más peligroso en una DLL es que la DLL se descargue mientras su código de devolución de llamada todavía puede ejecutarse. Si se ejecuta código descargado, se produce una violación de acceso.

La regla básica es dejar de enviar, esperar la finalización de las devoluciones de llamada y cerrar los objetos en la función de apagado explícita de la DLL antes de descargar. Ya sea con las funciones de espera individuales o con el grupo de limpieza del capítulo 6, esta comprobación de finalización no debe omitirse.6

No realice esta espera dentro de DllMain. Por su relación con el bloqueo del cargador provoca otro interbloqueo. Consulte el artículo sobre DllMain y el bloqueo del cargador para más detalle.

7.2 Empareje FreeLibraryWhenCallbackReturns con una referencia tomada antes de enviar

Para la situación «esta devolución de llamada es el último trabajo, así que quiero soltar la referencia de la DLL después de que regrese», existe FreeLibraryWhenCallbackReturns.4

Sin embargo, esta API por sí sola no impide una descarga antes de que empiece la devolución de llamada. Lo que hace es soltar una referencia de módulo cuando la devolución de llamada en ejecución regresa. Úsela en pareja: tome una referencia de módulo para ese procesamiento con GetModuleHandleEx antes de enviar, y suéltela desde la devolución de llamada con esta API.

Cerrar la DLL en una función de apagado frente a devolver una referencia desde la devolución de llamadaLa forma básica es una función de apagado distinta de DllMain que detiene los envíos, espera la finalización y libera antes de descargar la DLL; en un diseño en el que la última devolución de llamada devuelve la referencia, tomar la referencia con GetModuleHandleEx antes de enviar y emparejarla con la liberación tras el retorno mediante FreeLibraryWhenCallbackReturnsFunción de apagado explícitaDejar de enviar, esperar, liberarDespués descargar la DLLNo esperar en DllMainTomar una referencia de módulo antes de enviarEnviar la devolución de llamadaProgramar la liberación dentro de la devolución de llamadaFreeLibraryWhenCallbackReturnsSoltar una referencia después de volver

Figura 12: No confunda la espera de finalización en la función de apagado con soltar la referencia tomada para la devolución de llamada; en ambos casos, diseñe primero la vida de la DLL.

8. Resumen — empiece por work y sustituya junto con el apagado

El grupo de hilos de Win32 es la base que cambia la concurrencia en código nativo de «crear hilos» a «entregar trabajos como devoluciones de llamada». Puede consolidar los trabajos de corta duración y los hilos que solo esperan, y dejar la gestión de hilos al sistema operativo.

La adopción puede ser gradual. Primero, con work, hacer de la creación, el envío, detener los envíos, esperar la finalización y la liberación un conjunto. A continuación, sustituir los hilos que solo esperan por wait, y los hilos que solo temporizan por timer. Considere la integración de io y la división de grupos cuando resulten necesarias.

Migrar el código existente al grupo de hilos por etapasRevisar los hilos autogestionados existentes, conservar el trabajo que encaja en la biblioteca estándar o en un hilo dedicado, pasar el trabajo adecuado al grupo a work incluyendo detener los envíos y esperar la finalización, y sustituir por etapas las esperas por wait y los temporizadores por timerNoSíRevisar los hilos autogestionados existentes¿Encaja en el grupo?Elegir biblioteca estándar o dedicadoPasar a work, apagado incluido en el conjuntoEsperas a wait, temporizadores a timerIntegración de E/S y división de grupos según haga falta

Figura 13: No basta con reducir hilos; completar el apagado en cada etapa es la base de una migración gradual.

Lo que se mantiene hasta el final son tres disciplinas: no bloquear durante mucho tiempo, no esperar de forma síncrona en el mismo grupo y no contaminar el estado del hilo. En una DLL, además, evite la carrera con la descarga. Deje a C++ estándar o a .NET lo que ellos cubren, y use esta API donde haga falta integración o control específicos de Windows.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se encarga del diseño de migración de código nativo cuyos hilos han proliferado hacia el grupo de hilos, de revisiones de diseño del procesamiento concurrente en aplicaciones y DLL de C++, y de la investigación de la causa de cuelgues y fallos provocados por hambruna del grupo o por devoluciones de llamada. Puede consultarnos empezando por un inventario del código existente.

Referencias

  1. Microsoft Learn, Thread Pools. Sobre que un grupo de hilos es una colección de hilos trabajadores que ejecutan de forma eficiente devoluciones de llamada asíncronas en nombre de la aplicación; sobre los tipos de aplicación a los que encaja (emitir en paralelo un gran número de elementos de trabajo pequeños, crear y destruir con frecuencia hilos de corta duración, procesar trabajo independiente en paralelo, esperas exclusivas de objetos del kernel, etc.); y sobre el rediseño completo de Vista (unificación de tipos de hilo trabajador, una sola cola de temporizador, hilos persistentes dedicados, grupos de limpieza, varios grupos en un proceso y la API nueva). ↩ ↩2 ↩3 ↩4 ↩5

  2. Microsoft Learn, <future>. Sobre que la ejecución asíncrona por tarea mediante std::async y future se ofrece como biblioteca estándar, de modo que se puede escribir concurrencia sin gestionar hilos de forma directa. ↩

  3. Microsoft Learn, Thread Pooling. Sobre la estructura de la API antigua del grupo de hilos (QueueUserWorkItem, colas de temporizador, esperas registradas, BindIoCompletionCallback); sobre que no hay forma de cancelar un trabajo una vez encolado; y sobre que la API nueva del grupo de hilos introducida en Vista se afirma explícitamente como más sencilla y superior en fiabilidad, rendimiento y flexibilidad. ↩ ↩2

  4. Microsoft Learn, threadpoolapiset.h header. Sobre la lista de funciones que incluye las cuatro funciones de creación de objetos CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait y CreateThreadpoolIo; los grupos de limpieza (CreateThreadpoolCleanupGroup); y la limpieza ligada a la finalización de la devolución de llamada (LeaveCriticalSectionWhenCallbackReturns, FreeLibraryWhenCallbackReturns, etc.). ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). Sobre crear un objeto work a partir de una función de devolución de llamada y un puntero de contexto; y sobre que el tercer argumento, TP_CALLBACK_ENVIRON, puede indicar el entorno de ejecución de la devolución de llamada (el grupo al que pertenece, etc.), y NULL significa que se ejecuta en el entorno predeterminado. ↩ ↩2 ↩3

  6. Microsoft Learn, Using the Thread Pool Functions. Sobre el procedimiento básico de crear con CreateThreadpoolWork, enviar con SubmitThreadpoolWork, esperar la finalización con WaitForThreadpoolWorkCallbacks y cerrar con CloseThreadpoolWork; y sobre un ejemplo de configuración que combina un grupo personalizado con un entorno de devolución de llamada y un grupo de limpieza. ↩ ↩2 ↩3 ↩4

  7. Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). Sobre poder enviar el mismo objeto work varias veces sin esperar a que termine una devolución de llamada anterior, de modo que las devoluciones de llamada se ejecutan en paralelo; y sobre que el grupo puede ajustar (limitar) el número de hilos por eficiencia. ↩

  8. Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). Sobre notificar al grupo que la devolución de llamada actual puede ejecutarse durante mucho tiempo, de modo que el grupo use eso para decidir si obtener hilos para otras devoluciones de llamada; y sobre considerar un hilo dedicado para una devolución de llamada de larga duración siempre que sea posible. ↩ ↩2

  9. Microsoft Learn, Thread Pooling. Sobre que los elementos de trabajo enviados a un grupo de hilos, y las funciones que llaman, tienen que ser seguros para el grupo; sobre no asumir que el hilo que ejecuta es un hilo dedicado y persistente; y sobre evitar el uso de TLS y de llamadas asíncronas que requieren un hilo persistente. ↩

  10. Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). Sobre poder fijar un tope al número de hilos trabajadores de un grupo creado con CreateThreadpool (el suelo es SetThreadpoolThreadMinimum). ↩

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué tiene de mejor un grupo de hilos frente a crear hilos propios con CreateThread?
La eficiencia cuando hay que procesar un gran número de trabajos de corta duración, y menos código de gestión de hilos. Crear y destruir un hilo tiene un coste que no se puede ignorar. Una aplicación que repite «CreateThread por cada trabajo y destruirlo al terminar», o que mantiene muchos hilos que solo duermen para esperar un evento, puede reducir el número de hilos y los cambios de contexto al pasar a un grupo. La documentación oficial también enumera como candidatas a un grupo las aplicaciones que emiten en paralelo un gran número de elementos de trabajo pequeños, las que crean muchos hilos de corta duración y las que tienen hilos dedicados únicamente a esperar objetos del kernel. A la inversa, el trabajo en el que «el propio hilo necesita una personalidad» —un cambio de prioridad, una STA de COM, un procesamiento dedicado de larga duración— debe seguir en un hilo dedicado como hasta ahora.
¿En qué se diferencia de las funciones antiguas del grupo de hilos como QueueUserWorkItem?
El grupo de hilos se rediseñó por completo en Windows Vista. Las API actuales de la familia threadpoolapiset (CreateThreadpoolWork y similares) son la API nueva; QueueUserWorkItem, RegisterWaitForSingleObject y similares son la API antigua (heredada). La API nueva unifica los tipos de hilo trabajador, permite crear varios grupos independientes en un proceso y ofrece mecanismos como la liberación masiva mediante un grupo de limpieza y la liberación de un bloqueo o la descarga de una DLL ligada a la finalización de la devolución de llamada. La documentación oficial también indica que la API nueva es más sencilla y superior en fiabilidad, rendimiento y flexibilidad. La API antigua tiene además restricciones estructurales, como «no hay forma de cancelar un trabajo una vez encolado», así que en código nuevo use la API nueva.
¿Hay cosas que no se deben hacer dentro de una devolución de llamada?
Hay tres principales. Primera: bloquear durante mucho tiempo o ejecutar un procesamiento largo con los valores predeterminados. El grupo ajusta su número de hilos asumiendo que las devoluciones de llamada terminan pronto; para un procesamiento largo, declárelo con CallbackMayRunLong o use un hilo dedicado. Segunda: esperar de forma síncrona la finalización de otro trabajo enviado al mismo grupo. Si todos los trabajadores terminan «esperando a otro trabajador», se produce un interbloqueo por hambruna del grupo. Tercera: depender de la personalidad del hilo. Los hilos trabajadores se comparten entre devoluciones de llamada; devolver con una prioridad de hilo o un estado de inicialización COM cambiados, o dejar estado en TLS, contamina la siguiente devolución de llamada. Para la limpieza al final (liberar un bloqueo o descargar una DLL) se ofrecen mecanismos dedicados como LeaveCriticalSectionWhenCallbackReturns y FreeLibraryWhenCallbackReturns.
¿Hay puntos a vigilar al usar el grupo de hilos dentro de una DLL?
El mayor peligro es «la DLL se descarga mientras una devolución de llamada todavía puede ejecutarse». Si la devolución de llamada se ejecuta después de la descarga, se produce una violación de acceso. En su procesamiento de apagado, la DLL debe esperar de forma fiable la finalización de las devoluciones de llamada que emitió —con una función de espera como WaitForThreadpoolWorkCallbacks, o CloseThreadpoolCleanupGroupMembers en un grupo de limpieza— antes de cerrar los objetos. Esperar esto dentro de DllMain, sin embargo, puede interbloquearse por interacción con el bloqueo del cargador, así que la regla es hacerlo en una función de apagado explícita, no en DllMain. Para el caso en que la propia devolución de llamada es «el último trabajo» y quiere liberar la DLL, se ofrece una API dedicada, FreeLibraryWhenCallbackReturns.
Ahora que C++ tiene std::async y .NET tiene ThreadPool, ¿sigue habiendo ocasiones para usar esta API de forma directa?
Sí. El criterio es «si basta la herramienta de esa capa». Si la granularidad de concurrencia que se necesita en C++ la cubren std::async o std::thread, la biblioteca estándar es la primera candidata también desde el punto de vista de la portabilidad. Por otro lado, querer unificar temporizadores, esperas de objetos del kernel y finalizaciones de E/S asíncrona en un solo mecanismo de devolución de llamada; querer dividir grupos y controlar el número de hilos por tipo de trabajo; no querer mantener hilos propios dentro de una DLL o un componente COM: esos requisitos son el ámbito del grupo de hilos de Win32. La relación con el ThreadPool de .NET y IOCP se trata en un artículo relacionado, y mientras se escriba nativo, conocer este mecanismo que está en la capa de debajo no es tiempo perdido.

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.

Volver al blog