Concurrencia y patrones de error ocultos en el código: punto muerto

Seguramente, muchos escucharon, pero alguien conoció en la práctica, palabras como puntos muertos y condiciones de carrera. Estos conceptos se clasifican como errores en el uso de la concurrencia. Si le hago una pregunta sobre qué es un punto muerto, es muy probable que comience a dibujar una imagen clásica de punto muerto o su representación en pseudocódigo sin ninguna duda. Algo como esto:



Obtenemos esta información en el instituto; se puede encontrar en libros y artículos en Internet. Tal punto muerto usando, por ejemplo, dos mutexes, en todo su esplendor se puede encontrar en el código. Pero en la mayoría de los casos, no todo es tan simple, y no todos pueden ver el patrón de error clásico en el código si no se presenta en la forma habitual.



Considere una clase en la que estamos interesados ​​en los métodos StartUpdate, CheckAndUpdate y Stop, se usa C ++, el código es lo más simple posible:

std::recursive_mutex m_mutex; Future m_future; void Stop() { std::unique_lock scoped_lock(m_mutex); m_future.Wait(); // do something } void StartUpdate() { m_future.Wait(); m_future = Future::Schedule(std::bind(&Element::CheckAndUpdate, this), std::chrono::milliseconds(100); } void CheckAndUpdate() { std::unique_lock scoped_lock(m_mutex); //do something } 

A qué debe prestar atención en el código presentado:

  1. Se utiliza mutex recursivo. La captura repetida de un mutex recursivo no genera expectativas solo si estas capturas ocurren en el mismo hilo. En este caso, el número de exenciones de mutex debería corresponder al número de capturas. Si intentamos capturar un mutex recursivo que ya está capturado en otro hilo, el hilo pasa al modo de espera.
  2. La función Future :: Schedule comienza (en n milisegundos) en un subproceso separado que le pasó la devolución de llamada

Ahora analizamos toda la información recibida y componimos una imagen:



Teniendo en cuenta los dos hechos presentados anteriormente, no es difícil concluir que un intento de capturar un mutex recursivo en una de las funciones llevará a la expectativa de la liberación del mutex si ya se capturó en otra función, ya que la devolución de llamada CheckAndUpdate siempre se ejecuta en un hilo separado.

A primera vista, no hay nada sospechoso con respecto al punto muerto. Pero para estar más cerca, todo se reduce a nuestra imagen clásica. Cuando el objeto funcional comienza a ejecutarse, capturamos implícitamente el recurso m_future, la devolución de llamada directamente
asociado con m_future:



La secuencia de acciones que conducen al punto muerto es la siguiente:

  1. Está previsto ejecutar CheckAndUpdate, pero la devolución de llamada no se inicia inmediatamente, después de n milisegundos.
  2. Se llama al método Stop, y luego comienza: intentamos capturar el mutex - el recurso es uno capturado, comenzamos a esperar a que se complete m_future - el objeto aún no ha sido llamado, estamos esperando.
  3. Comienza la ejecución de CheckAndUpdate: tratamos de capturar el mutex; no podemos, el recurso ya está capturado por otro hilo, estamos esperando su lanzamiento.

Eso es todo: el subproceso que realiza la llamada Stop espera a que CheckAndUpdate se complete, y el otro subproceso, a su vez, no puede continuar funcionando hasta que toma el mutex que ya ha capturado el subproceso mencionado anteriormente. Es un punto muerto bastante clásico. La mitad del trabajo está hecho: se ha descubierto la causa del problema.

Ahora un poco sobre cómo solucionarlo.
Enfoque 1
El procedimiento para capturar recursos debe ser el mismo, esto evitará puntos muertos. Es decir, debe ver si es posible cambiar el orden de captura de recursos en el método Stop. Como aquí el caso de punto muerto no es del todo obvio, y no hay una captura explícita del recurso m_future en CheckAndUpdate, decidimos pensar en otra solución para evitar la devolución del error en el futuro.

Enfoque 2
  1. Compruebe si puede optar por no usar el mutex en CheckAndUpdate.
  2. Como usamos el mecanismo de sincronización, restringimos el acceso a algunos recursos. Quizás sea suficiente para usted rehacer estos recursos en atómicos (como teníamos), cuyo acceso ya es seguro para subprocesos.
  3. Resultó que las variables, cuyo acceso era limitado, se pueden convertir fácilmente a atómicas, por lo que el mutex mencionado se elimina con éxito.


Aquí hay un ejemplo tan directo con un punto muerto no obvio que se reduce fácilmente al patrón de este error. ¡Finalmente, quiero desearle que escriba un código confiable y seguro para subprocesos!

Source: https://habr.com/ru/post/442448/


All Articles