Prompt y contexto
Implemente un worker en segundo plano que consuma una cola. Durante el apagado o la cancelación ascendente, el hilo principal solicita la detención; una tarea iniciada se completa, mientras que el trabajo no iniciado se retiene o se descarta explícitamente. Evite la espera activa, las condiciones de carrera de datos, un hilo vivo durante la destrucción y una espera en variable de condición que nunca retorne.
En C++20, std::jthread solicita la detención y realiza el join durante la destrucción, y puede inyectar un std::stop_token en la función de entrada. Una solicitud de detención es cooperativa; no puede terminar código arbitrario por la fuerza. El worker debe realizar sondeos (polling) o esperar con una primitiva consciente de la detención.
Qué evalúa el entrevistador
- Que comprenda que un token de detención es una solicitud sobre un estado compartido, no una terminación asíncrona de hilos.
- Que utilice la unión automática de
std::jthready mantenga vivos los miembros del objeto mientras el hilo accede a ellos. - Que haga que las esperas bloqueantes sean interrumpibles, por ejemplo, con la sobrecarga de stop-token de
condition_variable_any. - Que mantenga seguros los tiempos de vida de la cola, el estado de detención, las excepciones de tareas y la limpieza.
- Que pruebe la detención con cola vacía, carreras de detención, excepciones de tareas, llamadas repetidas a
request_stopy el orden de destrucción.
Preguntas para aclarar primero
- ¿Puede terminar una tarea en ejecución, y es idempotente o tiene efectos secundarios externos?
- Cuando se cierra la cola, ¿las tareas no consumidas se descartan, se transfieren o son procesadas por otro worker?
- ¿La espera es interrumpible o depende de una llamada de E/S de terceros que no se puede cancelar?
- ¿Las excepciones se registran, se propagan al hilo que realiza el join o se utilizan para detener el servicio?
- ¿Pueden múltiples hilos llamar a la detención, destrucción o reinicio?
Una respuesta de 30 segundos
“Tome posesión del worker con std::jthread y acepte un std::stop_token en su función de entrada. Proteja la cola con un mutex y espere utilizando un condition_variable_any consciente de la detención; tras despertar, verifique la detención, el cierre de la cola y el estado de la tarea. Una tarea desencolada se ejecuta hasta puntos de cancelación definidos y realiza la limpieza. La destrucción solicita la detención y hace join; nunca hace detach. El estado compartido sobrevive al hilo. Las pruebas cubren carreras de detención y excepciones.”
Solución paso a paso
Paso 1: Definir el contrato de cancelación
Separe “detención solicitada” de “tarea completada”. Una solicitud de detención evita nuevo trabajo; una tarea iniciada termina o devuelve un resultado de cancelación en puntos seguros. No prometa la cancelación inmediata de llamadas arbitrarias de terceros.
Paso 2: Definir la propiedad
Mantenga el objeto que posee la cola, el mutex, la variable de condición y el std::jthread vivo por más tiempo que el hilo. La destrucción solicita la detención y espera antes de liberar los miembros. Nunca capture una referencia a un ámbito destruido ni publique un this crudo en un callback tardío.
Paso 3: Hacer que las esperas sean cancelables
Utilice la espera con stop-token de condition_variable_any o registre un stop_callback que llame a notify_all. El predicado verifica si la cola no está vacía, el estado cerrado y stop_requested(); los despertares vuelven a adquirir el bloqueo y recomprueban el estado.
std::jthread worker([this](std::stop_token st) {
for (;;) {
Task task;
{
std::unique_lock lock(mu_);
cv_.wait(lock, st, [this, &st] {
return closed_ || !queue_.empty() || st.stop_requested();
});
if (st.stop_requested() || (closed_ && queue_.empty())) return;
task = std::move(queue_.front());
queue_.pop_front();
}
run(task, st);
}
});Paso 4: Manejar los puntos de detención y las excepciones de las tareas
Verifique el token entre las fases de la tarea y defina los límites de los efectos secundarios para evitar escrituras parciales. Capture las excepciones en el límite del hilo, registre el ID de la tarea y el error, y decida si continuar o detener el worker. No permita que una excepción escape de la función de entrada.
Paso 5: Cerrar en el orden correcto
Rechace nuevas tareas, marque la cola como cerrada, notifique a los que están esperando, solicite la detención, haga join y solo entonces libere los recursos. Defina el tiempo de espera para el vaciado y el manejo de tareas sobrantes. Llamadas repetidas a request_stop() deben ser seguras y no deben repetir efectos secundarios.
Paso 6: Probar carreras y observar el comportamiento
Pruebe la detención con cola vacía, la detención durante el desencolado, llamadas de detención simultáneas, notificación durante la destrucción, excepciones de tareas, tiempo de espera de E/S bloqueante y cierres repetidos. Registre la latencia de detención, tareas completadas/canceladas, la cola restante, excepciones y tiempo de join; ejecute ThreadSanitizer para detectar condiciones de carrera.
Una respuesta de ejemplo sólida
“Administro el ciclo de vida del worker con jthread y acepto stoptoken. Un mutex protege el estado de la cola; un conditionvariable_any consciente de la detención verifica si está cerrada, no vacía y la solicitud de detención, de modo que la detención despierta la espera. Después de desencolar, libero el bloqueo. La tarea verifica el token en puntos seguros y finaliza la limpieza de la transacción. La función de entrada captura y registra las excepciones.”
“El apagado rechaza nuevo trabajo, establece el estado cerrado, notifica, solicita la detención y hace join. Nunca hace detach ni libera la cola y el registrador (logger) antes de tiempo. Las pruebas cubren una cola vacía, carreras de desencolado, excepciones, detenciones repetidas, tiempos de espera de tareas largas y ThreadSanitizer.”
Errores comunes
- Tratar stop_token como una terminación forzada → los recursos y las transacciones se rompen → defina puntos cooperativos.
- Olvidar hacer join a un
std::thread→ terminación o un hilo colgante → utilice jthread o una propiedad explícita del ciclo de vida. - Esperar una sola notificación → una notificación perdida duerme para siempre → utilice un bucle de predicado y despierte al detener.
- Ejecutar tareas mientras se mantiene el bloqueo → los productores y el apagado se bloquean → libere después de desencolar.
- Dejar que las excepciones escapen de la función de entrada → el proceso termina → capture en el límite del hilo.
- Liberar miembros antes de detener el hilo → uso después de la liberación (use-after-free) → detenga, haga join y luego libere el estado.
Preguntas de seguimiento y respuestas
¿Qué hace el destructor de un jthread?
Si es joinable, la destrucción solicita la detención y realiza el join; no termina la tarea por la fuerza. La tarea debe responder, por lo que join puede esperar a un punto seguro.
¿Puede una solicitud de detención despertar una variable de condición?
La sobrecarga de stop-token de condition_variable_any retorna cuando se solicita la detención. Una espera personalizada necesita un callback de detención para notificar y un predicado que vuelva a verificar el estado.
¿Se puede cancelar inmediatamente una escritura de base de datos en curso?
No asuma eso. Utilice pasos reversibles (con rollback) o idempotentes, el soporte de tiempo de espera/cancelación del controlador (driver) y una verificación de detención en los límites del commit.
¿Cómo se evitan las condiciones de carrera de detención?
Trate cerrado, la cola y la detención como un solo protocolo de ciclo de vida. Realice transiciones bajo el bloqueo y notifique después de los cambios de estado; fuerce la ventana donde la detención y el desencolado ocurren juntos y ejecute ThreadSanitizer.