Tema representativo de entrevista

Entrevista de C++: Implementar cancelación cooperativa con std::jthread y stop_token

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Implemente un worker en C++20 que consuma tareas en un bucle mientras el hilo principal puede solicitar la detención. No debe perder una tarea iniciada, bloquearse para siempre ni acceder al estado compartido después de la destrucción. Explique std::jthread, stop_token, condition_variable_any, excepciones y pruebas.

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::jthread y 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_stop y 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.

cpp
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.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta