En mi nuevo equipo que administro, la mayoría de nuestro código es plataforma, socket TCP y código de red http. Todos los C ++. La mayor parte se originó de otros desarrolladores que han abandonado el equipo. Los desarrolladores actuales en el equipo son muy inteligentes, pero en su mayoría junior en términos de experiencia.
Nuestro mayor problema: errores de concurrencia multiproceso. La mayoría de nuestras bibliotecas de clases están escritas para ser asíncronas mediante el uso de algunas clases de grupo de subprocesos. Los métodos en las bibliotecas de clases a menudo ponen en cola tareas largas en el grupo de subprocesos desde un subproceso y luego los métodos de devolución de llamada de esa clase se invocan en un subproceso diferente. Como resultado, tenemos muchos errores de casos extremos que implican suposiciones de subprocesos incorrectas. Esto da como resultado errores sutiles que van más allá de solo tener secciones y bloqueos críticos para proteger contra problemas de concurrencia.
Lo que hace que estos problemas sean aún más difíciles es que los intentos de solucionarlos a menudo son incorrectos. Algunos errores que he observado que el equipo intenta (o dentro del código heredado) incluye algo como lo siguiente:
Error común n. ° 1 : solucionar el problema de concurrencia simplemente bloqueando los datos compartidos, pero olvidando lo que sucede cuando los métodos no se llaman en el orden esperado. Aquí hay un ejemplo muy simple:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Entonces, ahora tenemos un error en el que se puede llamar a Shutdown mientras OnHttpNetworkRequestComplete está ocurriendo. Un probador encuentra el error, captura el volcado de memoria y asigna el error a un desarrollador. Él a su vez corrige el error así.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
La solución anterior se ve bien hasta que te das cuenta de que hay un caso de borde aún más sutil. ¿Qué sucede si se llama a Shutdown antes de que se vuelva a llamar a OnHttpRequestComplete? Los ejemplos del mundo real que tiene mi equipo son aún más complejos, y los casos extremos son aún más difíciles de detectar durante el proceso de revisión del código.
Error común n. ° 2 : solucionar problemas de punto muerto al salir ciegamente de la cerradura, esperar a que termine el otro subproceso, luego volver a ingresar al candado, ¡pero sin manejar el caso de que el otro subproceso acaba de actualizar el objeto!
Error común n. ° 3 : aunque los objetos se cuentan por referencia, la secuencia de apagado "libera" su puntero. Pero se olvida de esperar el hilo que aún se está ejecutando para liberar su instancia. Como tal, los componentes se apagan limpiamente, luego se invocan devoluciones de llamada espurias o tardías en un objeto en un estado que no espera más llamadas.
Hay otros casos extremos, pero la conclusión es esta:
La programación multiproceso es sencillamente difícil, incluso para personas inteligentes.
Al detectar estos errores, paso tiempo discutiendo los errores con cada desarrollador para desarrollar una solución más adecuada. Pero sospecho que a menudo están confundidos sobre cómo resolver cada problema debido a la enorme cantidad de código heredado que la solución "correcta" implicará tocar.
Pronto enviaremos, y estoy seguro de que los parches que estamos aplicando se mantendrán para el próximo lanzamiento. Después, tendremos algo de tiempo para mejorar la base del código y refactorizar donde sea necesario. No tendremos tiempo para volver a escribir todo. Y la mayoría del código no es tan malo. Pero estoy buscando refactorizar el código de modo que los problemas de subprocesos se puedan evitar por completo.
Un enfoque que estoy considerando es este. Para cada característica importante de la plataforma, tenga un hilo único dedicado donde todos los eventos y devoluciones de llamadas de la red se agrupen. Similar al subproceso de apartamentos COM en Windows con el uso de un bucle de mensajes. Las operaciones de bloqueo largas aún podrían enviarse a un subproceso de grupo de trabajo, pero la devolución de llamada de finalización se invoca en el subproceso del componente. Los componentes podrían incluso compartir el mismo hilo. Entonces, todas las bibliotecas de clases que se ejecutan dentro del subproceso se pueden escribir bajo la suposición de un mundo único con subprocesos.
Antes de seguir ese camino, también estoy muy interesado si existen otras técnicas estándar o patrones de diseño para tratar problemas de subprocesos múltiples. Y tengo que enfatizar, algo más allá de un libro que describe los conceptos básicos de mutexes y semáforos. ¿Qué piensas?
También estoy interesado en cualquier otro enfoque para adoptar un proceso de refactorización. Incluyendo cualquiera de los siguientes:
Literatura o documentos sobre patrones de diseño en torno a hilos. Algo más allá de una introducción a mutexes y semáforos. Tampoco necesitamos paralelismo masivo, solo formas de diseñar un modelo de objeto para manejar eventos asincrónicos de otros hilos correctamente .
Formas de diagramar el enhebrado de varios componentes, para que sea fácil estudiar y desarrollar soluciones. (Es decir, un equivalente UML para discutir hilos a través de objetos y clases)
Educar a su equipo de desarrollo sobre los problemas con el código multiproceso.
¿Qué harías?