c++ - simétrico - programacion multiprocesos
¿Consejos para convertir una aplicación monolítica grande y única en una arquitectura multiproceso? (15)
El producto principal de mi empresa es una gran aplicación monolítica de C ++, que se utiliza para el procesamiento y la visualización de datos científicos. Su base de código data de hace 12 o 13 años, y aunque hemos trabajado para actualizarla y mantenerla (el uso de STL y Boost, cuando me uní a la mayoría de los contenedores fueron personalizados, por ejemplo, me actualicé totalmente a Unicode y al VCL 2010, etc.) hay un problema restante, muy significativo: es completamente único. Dado que se trata de un programa de procesamiento y visualización de datos, esto se está convirtiendo cada vez más en una desventaja.
Soy el desarrollador y el gerente de proyecto para el próximo lanzamiento en el que queremos abordar esto, y este va a ser un trabajo difícil en ambas áreas. Estoy buscando consejos concretos, prácticos y arquitectónicos sobre cómo abordar el problema.
El flujo de datos del programa podría ser algo como esto:
- una ventana necesita dibujar datos
- En el método de pintura, llamará a un método GetData, a menudo cientos de veces para cientos de bits de datos en una operación de pintura
- Esto irá y calculará o leerá desde un archivo o cualquier otra cosa que se requiera (a menudo un flujo de datos bastante complejo; piense en esto como datos que fluyen a través de un gráfico complejo, cada nodo del cual realiza operaciones)
Es decir, el controlador de mensajes de pintura se bloqueará mientras se realiza el procesamiento, y si los datos aún no se han calculado y almacenado en la memoria caché, esto puede tardar mucho tiempo. Algunas veces esto son minutos. Se producen rutas similares para otras partes del programa que realizan operaciones de procesamiento prolongadas: el programa no responde durante todo el tiempo, a veces horas.
Estoy buscando consejos sobre cómo abordar el cambio de esto. Ideas prácticas Tal vez cosas como:
- patrones de diseño para solicitar datos de forma asíncrona?
- ¿almacena grandes colecciones de objetos para que los hilos puedan leer y escribir de forma segura?
- ¿Manejando la invalidación de conjuntos de datos mientras algo intenta leerlo?
- ¿Hay patrones y técnicas para este tipo de problema?
- ¿Qué debería estar preguntando que no había pensado?
No he hecho ninguna programación multiproceso desde mi Uni días hace unos años, y creo que el resto de mi equipo está en una posición similar. Lo que sabía era académico, no práctico, y no es suficiente para tener confianza acercándose a esto.
El objetivo final es tener un programa totalmente receptivo, donde todos los cálculos y la generación de datos se realicen en otros hilos y la IU siempre responda. Es posible que no lleguemos allí en un solo ciclo de desarrollo :)
Editar: pensé que debería agregar un par de detalles más sobre la aplicación:
- Es una aplicación de escritorio de 32 bits para Windows. Cada copia tiene licencia. Planeamos mantenerlo como una aplicación de escritorio que se ejecuta localmente
- Utilizamos Embarcadero (anteriormente Borland) C ++ Builder 2010 para el desarrollo. Esto afecta las bibliotecas paralelas que podemos usar, ya que la mayoría parece (?) Escribirse solo para GCC o MSVC. Afortunadamente, lo están desarrollando activamente y su compatibilidad con los estándares de C ++ es mucho mejor de lo que solía ser. El compilador admite estos componentes de Boost .
- Su arquitectura no es tan limpia como debería y los componentes a menudo están demasiado unidos. Este es otro problema :)
Editar # 2: ¡ Gracias por las respuestas hasta ahora!
- Me sorprende que tantas personas hayan recomendado una arquitectura multiproceso (es la respuesta más votado en este momento), no multihilo. Mi impresión es que es una estructura de programa muy Unix-ish, y no sé nada sobre cómo está diseñada o funciona. ¿Hay buenos recursos disponibles al respecto en Windows? ¿Es realmente tan común en Windows?
- En términos de enfoques concretos para algunas de las sugerencias de subprocesamiento múltiple, existen patrones de diseño para solicitudes asincrónicas y consumo de datos, o amenazas o sistemas MVP asíncronos, o cómo diseñar un sistema orientado a tareas, o artículos y libros y deconstrucciones posteriores al lanzamiento. ¿ilustrando cosas que funcionan y cosas que no funcionan? Podemos desarrollar toda esta arquitectura nosotros mismos, por supuesto, pero es bueno trabajar a partir de lo que otros han hecho antes y saber qué errores y trampas debemos evitar.
- Un aspecto que no se menciona en ninguna respuesta es el proyecto que maneja esto. Mi impresión es estimar cuánto tiempo llevará esto y mantener un buen control del proyecto cuando haga algo tan incierto como difícil. Esa es una de las razones por las que estoy buscando recetas o consejos de codificación práctica, supongo, para guiar y restringir la dirección de codificación tanto como sea posible.
Todavía no he marcado una respuesta para esta pregunta, no es por la calidad de las respuestas, lo cual es genial (y gracias) pero simplemente por el alcance de esto espero más respuestas o discusiones. ¡Gracias a los que ya respondieron!
- No intente multirreprobar todo en la aplicación anterior. Multithreading con el fin de decir que es multiproceso es una pérdida de tiempo y dinero. Estás construyendo una aplicación que hace algo, no un monumento para ti.
- Perfile y estudie sus flujos de ejecución para descubrir dónde pasa la mayor parte de su tiempo la aplicación. Un generador de perfiles es una gran herramienta para esto, pero también lo es recorrer el código en el depurador. Usted encuentra las cosas más interesantes en caminatas al azar.
- Desacoplar la interfaz de usuario de los cálculos de larga ejecución. Utilice técnicas de comunicación de hilos cruzados para enviar actualizaciones a la interfaz de usuario desde el hilo de cálculo.
- Como efecto secundario del n. ° 3: piense detenidamente sobre la reentrada: ahora que el cálculo se ejecuta en segundo plano y el usuario puede husmear en la interfaz de usuario, ¿qué elementos de la interfaz de usuario deberían desactivarse para evitar conflictos con la operación en segundo plano? Permitir que el usuario elimine un conjunto de datos mientras se ejecuta un cálculo con esos datos es probablemente una mala idea. (Mitigación: el cálculo hace una instantánea local de los datos) ¿Tiene sentido que el usuario distribuya múltiples operaciones informáticas al mismo tiempo? Si se maneja bien, podría ser una característica nueva y ayudar a racionalizar el esfuerzo de retrabajo de la aplicación. Si se ignora, será un desastre.
- Identifique las operaciones específicas que son candidatas a ser introducidas en un hilo de fondo. El candidato ideal suele ser una sola función o clase que hace mucho trabajo (requiere un "montón de tiempo" para completar (más de unos pocos segundos) con entradas y salidas bien definidas, que no utiliza recursos globales, y no lo hace. no toque la IU directamente. Evalúe y priorice a los candidatos en función de cuánto trabajo se requeriría para adaptarlo a este ideal.
- En términos de gestión de proyectos, tome las cosas paso a paso. Si tiene varias operaciones que son candidatas fuertes para moverlas a una secuencia de fondo, y no tienen interacción entre ellas, pueden ser implementadas en paralelo por múltiples desarrolladores. Sin embargo, sería un buen ejercicio que todos participen en una conversión primero para que todos entiendan qué buscar y establecer sus patrones para la interacción de la interfaz de usuario, etc. Realice una reunión extendida de la pizarra para analizar el diseño y el proceso de extracción de la función en un hilo de fondo. Ve a implementar eso (juntas o distribuye piezas a individuos), luego vuelve a reunirte para unir todo y discutir descubrimientos y puntos débiles.
- El subprocesamiento múltiple es un dolor de cabeza y requiere una reflexión más cuidadosa que la codificación directa, pero dividir la aplicación en múltiples procesos genera muchos más dolores de cabeza, la OMI. El soporte de subprocesos y las primitivas disponibles son buenos en Windows, quizás mejor que en otras plataformas. Usalos, usalos a ellos.
- En general, no haga más de lo que se necesita. Es fácil implementar y sobrecompletar un problema mediante el lanzamiento de más patrones y bibliotecas estándar.
- Si nadie en su equipo ha realizado trabajo de subprocesamiento múltiple antes, haga un presupuesto para que un experto o fondos lo contraten como consultor.
Bueno, creo que esperas mucho en base a tus comentarios aquí. No pasará de minutos a milisegundos mediante multihilo. Lo máximo que puede esperar es la cantidad de tiempo actual dividida por la cantidad de núcleos. Dicho esto, tienes un poco de suerte con C ++. He escrito aplicaciones científicas multiprocesador de alto rendimiento, y lo que desea buscar es el bucle más vergonzosamente paralelo que puede encontrar. En mi código científico, la pieza más pesada es calcular entre 100 y 1000 puntos de datos. Sin embargo, todos los puntos de datos se pueden calcular independientemente de los demás. Luego puede dividir el ciclo usando openmp. Esta es la manera más fácil y más eficiente de ir. Si su compilador no es compatible con OpenMP, entonces le será muy difícil portar el código existente. Con openmp (si tienes suerte), es probable que solo tengas que agregar un par de #pragmas para obtener entre 4 y 8 veces el rendimiento. Aquí hay un ejemplo de StochFit
Entonces, hay una sugerencia en su descripción del algoritmo sobre cómo proceder:
a menudo un flujo de datos bastante complejo: piense en esto como datos que fluyen a través de un gráfico complejo, cada nodo del cual realiza operaciones
Me gustaría hacer que ese gráfico de flujo de datos sea literalmente la estructura que hace el trabajo. Los enlaces en el gráfico pueden ser colas seguras para subprocesos, los algoritmos en cada nodo pueden permanecer casi sin cambios, excepto que se envuelven en un subproceso que recoge los elementos de trabajo de una cola y deposita los resultados en uno. Puede ir un paso más allá y usar sockets y procesos en lugar de colas e hilos; esto le permitirá extenderse a través de múltiples máquinas si hay un beneficio en el rendimiento al hacer esto.
Luego, su pintura y otros métodos de GUI deben dividirse en dos: una mitad para poner en cola el trabajo y la otra mitad para dibujar o usar los resultados a medida que salen de la tubería.
Esto puede no ser práctico si la aplicación supone que los datos son globales. Pero si está bien contenido en las clases, como su descripción lo sugiere, esta podría ser la forma más sencilla de lograr su paralelismo.
Es difícil darle las pautas adecuadas. Pero...
La forma más fácil de salir de acuerdo conmigo es convertir su aplicación a ActiveX EXE ya que COM tiene soporte para Threading, etc. integrado directamente en él, su programa se convertirá automáticamente en la aplicación Multi Threading. Por supuesto, tendrá que hacer algunos cambios en su código. Pero esta es la manera más corta y segura de ir.
No estoy seguro, pero es probable que RichClient Toolset lib haga el truco para ti. En el sitio, el autor ha escrito:
También ofrece funciones gratuitas de carga / instalación de registro para ActiveX-Dlls y un enfoque de roscado nuevo y fácil de usar, que funciona con tuberías con nombre debajo del capó y funciona, por lo tanto, también en el proceso cruzado.
Por favor, míralo. Quién sabe, puede ser la solución adecuada para sus necesidades.
En cuanto a la gestión de proyectos, creo que puede seguir utilizando lo que se proporciona en su elección IDE integrándolo con SVN a través de complementos.
Me olvidé de mencionar que hemos completado una aplicación para el mercado de acciones que intercambia automáticamente (compra y vende según bajos y altos) en los scripts que están en la cartera de usuarios en función de un algoritmo que hemos desarrollado.
Al desarrollar este software, nos enfrentamos al mismo tipo de problema que ha ilustrado aquí. Para resolverlo, convertimos la aplicación en EXE de ActiveX y convertimos todas las partes que necesitan ejecutarse de forma paralela en las DLL de ActiveX. ¡No hemos usado libs de terceros para esto!
HTH
Espero que esto te ayude a comprender y convertir tu aplicación monolítica de una única hebra a múltiples hilos fácilmente. Lo siento es por otro lenguaje de programación pero, sin embargo, los principios explicados son los mismos en todas partes.
http://www.freevbcode.com/ShowCode.Asp?ID=1287
Espero que esto ayude.
Esto es lo que haría ...
Comenzaría perfilando tu y viendo:
1) qué es lento y cuáles son los caminos calientes 2) qué llamadas son reentrantes o profundamente anidadas
puede usar 1) para determinar dónde está la oportunidad para las aceleraciones y dónde comenzar a buscar la paralelización.
puede usar 2) para descubrir dónde es probable que esté el estado compartido y obtener una idea más profunda de cuánto se enredan las cosas.
Utilizaría un buen perfilador de sistema y un buen perfilador de muestreo (como el kit de herramientas de Windows perforamnce o las vistas de simultaneidad del generador de perfiles en Visual Studio 2010 Beta2; estos son ambos ''gratuitos'' en este momento).
Luego averiguaría cuál es el objetivo y cómo separar las cosas gradualmente en un diseño más limpio que sea más receptivo (mover el trabajo fuera del hilo de la interfaz de usuario) y más rendimiento (paralelizar porciones computacionalmente intensivas). Me centraría en la prioridad más alta y en los artículos más notables primero.
Si no tiene una buena herramienta de refactorización como VisualAssist, invierta en una: vale la pena. Si no está familiarizado con Michael Feathers o los libros de refactorización de Kent Beck, considere tomarlos prestados. Me aseguraré de que mis refactorizaciones estén bien cubiertas por pruebas unitarias.
No se puede mover a VS (recomendaría los productos que trabajo en la Biblioteca de Agentes Asincrónicos y Biblioteca de Patrones Paralelos, también puede usar TBB u OpenMP).
En el impulso, miraría con atención boost :: thread, la biblioteca asio y la biblioteca de señales.
Pediría ayuda / orientación / escucha cuando me quede atascado.
-Almiar
Hay algo de lo que nadie ha hablado aún, pero que es bastante interesante.
Se llama future
s. Un futuro es la promesa de un resultado ... veamos con un ejemplo.
future<int> leftVal = computeLeftValue(treeNode); // [1]
int rightVal = computeRightValue(treeNode); // [2]
result = leftVal + rightVal; // [3]
Es bastante simple:
Se
leftVal
un hilo que comienza a calcularleftVal
, tomándolo de un grupo, por ejemplo, para evitar el problema de inicialización.Mientras
leftVal
se está calculando, se calcularightVal
.Agregue los dos, esto puede bloquear si
leftVal
aún no se ha calculado y espere a que termine el cálculo.
El gran beneficio aquí es que es sencillo: cada vez que tiene un cálculo seguido de otro que es independiente y luego se une al resultado, puede usar este patrón.
Vea el artículo de Herb Sutter sobre los future
, estarán disponibles en el próximo C++0x
pero ya hay bibliotecas disponibles hoy, incluso si la sintaxis quizás no es tan bonita como yo le haría creer;)
Lo primero que debes hacer es separar tu GUI de tus datos, el segundo es crear una clase multiproceso.
PASO 1 - Interfaz gráfica de usuario adaptable
Podemos suponer que la imagen que está produciendo está contenida en el lienzo de un TImage. Puedes poner un TTimer simple en tu forma y puedes escribir código como este:
if (CurrenData.LastUpdate>CurrentUpdate)
{
Image1->Canvas->Draw(0,0,CurrenData.Bitmap);
CurrentUpdate=Now();
}
¡DE ACUERDO! ¡Lo sé! Está un poco sucio, pero es rápido y simple. El punto es que:
- Necesitas un Objeto que se crea en el hilo principal
- El objeto se copia en el Formulario que necesita, solo cuando se necesita y de manera segura (vale, se necesita una mejor protección para el mapa de bits, pero para la semplicidad ...)
- El objeto CurrentData es su proyecto real, de un solo hilo, que produce una imagen
Ahora tiene una GUI rápida y sensible. Si su algoritmo es lento, la actualización es lenta, pero su usuario nunca pensará que su programa está congelado.
PASO 2 - Multithread
Le sugiero que implemente una clase como la siguiente:
SimpleThread.h
typedef void (__closure *TThreadFunction)(void* Data);
class TSimpleThread : public TThread
{
public:
TSimpleThread( TThreadFunction _Action,void* _Data = NULL, bool RunNow = true );
void AbortThread();
__property Terminated;
protected:
TThreadFunction ThreadFunction;
void* Data;
private:
virtual void __fastcall Execute() { ThreadFunction(Data); };
};
SimpleThread.c
TSimpleThread::TSimpleThread( TThreadFunction _Action,void* _Data, bool RunNow)
: TThread(true), // initialize suspended
ThreadFunction(_Action), Data(_Data)
{
FreeOnTerminate = false;
if (RunNow) Resume();
}
void TSimpleThread::AbortThread()
{
Suspend(); // Can''t kill a running thread
Free(); // Kills thread
}
Vamos a explicar Ahora, en tu clase de subprocesos simple puedes crear un objeto como este:
TSimpleThread *ST;
ST=new TSimpleThread( RefreshFunction,NULL,true);
ST->Resume();
Vamos a explicar mejor: ahora, en tu propia clase monolítica, has creado un hilo. Más: trae una función (es decir, RefreshFunction) en un hilo separado . El alcance de su función es el mismo, la clase es la misma, la ejecución es separada.
Lo principal que tienes que hacer es desconectar tu UI de tu conjunto de datos. Sugeriría que la forma de hacerlo es poner una capa intermedia.
Tendrá que diseñar una estructura de datos de datos cocinados para mostrar. Es muy probable que contenga copias de algunos de sus datos de back-end, pero "cocinados" para que sean fáciles de extraer. La idea clave aquí es que esto es rápido y fácil de pintar. Incluso puede que esta estructura de datos contenga posiciones de pantalla calculadas de bits de datos para que se pueda extraer rápidamente.
Siempre que reciba un mensaje WM_PAINT, debe obtener la versión completa más reciente de esta estructura y extraer de ella. Si hace esto correctamente, debería poder manejar múltiples mensajes WM_PAINT por segundo porque el código de pintura nunca se refiere a sus datos finales. Simplemente está girando a través de la estructura cocida. La idea aquí es que es mejor pintar datos obsoletos rápidamente que colgar su UI.
Mientras tanto...
Debe tener 2 copias completas de esta estructura cocida para visualizar. Uno es lo que mira el mensaje WM_PAINT. (llámalo cfd_A ). El otro es lo que le das a tu función CookDataForDisplay (). (llámalo cfd_B ). Su función CookDataForDisplay () se ejecuta en una secuencia separada, y trabaja en la construcción / actualización de cfd_B en el fondo. Esta función puede tomar todo el tiempo que quiera porque no está interactuando con la pantalla de ninguna manera. Una vez que la llamada retorna, cfd_B será la versión más actualizada de la estructura.
Ahora intercambie cfd_A y cfd_B e InvalidateRect en la ventana de su aplicación.
Una forma simplista de hacer esto es hacer que su estructura cocida para visualizar sea un mapa de bits, y esa podría ser una buena forma de ir a rodar la pelota, pero estoy seguro que con un poco de pensamiento puede hacer mucho mejor trabajo con una estructura más sofisticada.
Entonces, refiriéndome a tu ejemplo.
- En el método de pintura, llamará a un método GetData, a menudo cientos de veces para cientos de bits de datos en una operación de pintura
Esto ahora es 2 hilos, el método de pintura se refiere a cfd_A y se ejecuta en el hilo de UI. Mientras tanto, cfd_B está siendo creado por un hilo de fondo utilizando llamadas GetData.
La forma rápida y sucia de hacer esto es
- Tome su código WM_PAINT actual, péguelo en una función llamada PaintIntoBitmap ().
- Cree un mapa de bits y una memoria DC, esto es cfd_B.
- Cree un hilo y páselo cfd_B y llámelo PaintIntoBitmap ()
- Cuando este hilo finalice, intercambie cfd_B y cfd_A
Ahora su nuevo método WM_PAINT simplemente toma el mapa de bits pre-renderizado en cfd_A y lo dibuja en la pantalla. Su interfaz de usuario ahora está desconectada de su función back-end GetData ().
Ahora comienza el trabajo real, porque la manera rápida y sucia no maneja el cambio de tamaño de ventana muy bien. Puede ir desde allí para refinar cuáles son sus estructuras cfd_A y cfd_B poco a poco hasta que llegue al punto en el que esté satisfecho con el resultado.
Mi sugerencia número uno, aunque es muy tarde (lo siento por revivir el hilo viejo, ¡es interesante!) Es buscar lazos de transformación homogéneos en los que cada iteración del ciclo mute una información completamente independiente de las otras iteraciones.
En lugar de pensar en cómo convertir esta antigua base de código en una asíncrona ejecutando todo tipo de operaciones en paralelo (lo que podría ser todo tipo de problemas, desde peor que un solo subproceso, desde patrones de bloqueo pobres o exponencialmente peores, condiciones de carrera / puntos muertos tratando de hacer esto en retrospectiva para codificar, no se puede comprender completamente), se adhieren a la mentalidad secuencial para el diseño general de la aplicación por ahora, pero identificar o extraer bucles de transformación simples y homogéneos. No vaya desde el intrusivo ancho multiproceso de nivel de diseño y luego intente profundizar en los detalles. Trabaje primero desde multihilo no intrusivo de detalles de implementación fina y puntos de acceso específicos.
Lo que quiero decir con bucles homogéneos es básicamente uno que transforma los datos de una manera muy directa, como:
for each pixel in image:
make it brighter
Eso es muy sencillo de razonar y puede paralelizar este ciclo de forma segura sin ningún tipo de problema utilizando OMP o TBB o lo que sea y sin enredarse en la sincronización del hilo. Solo se necesita echar un vistazo a este código para comprender completamente sus efectos secundarios.
Trate de encontrar tantos puntos de acceso como pueda que se ajusten a este tipo de bucle de transformación homogéneo simple y si tiene bucles complejos que actualizan muchos tipos diferentes de datos con flujos de control complejos que desencadenan efectos secundarios complejos, luego intente refactorizar hacia estos bucles homogéneos. A menudo, un bucle complejo que causa 3 efectos secundarios dispares a 3 tipos diferentes de datos se puede convertir en 3 bucles homogéneos simples que cada uno activa un solo tipo de efecto secundario a un tipo de datos con un flujo de control más simple. Hacer bucles múltiples en lugar de uno puede parecer un poco derrochador, pero los bucles se vuelven más simples, la homogeneidad a menudo conducirá a patrones de acceso a la memoria secuenciales más amigables con el caché versus patrones esporádicos de acceso aleatorio, y entonces tenderá a encontrar muchas más oportunidades para con seguridad paralelizar (así como vectorizar) el código de una manera directa.
Primero tienes que entender completamente los efectos secundarios de cualquier código que intentes paralelizar (¡y quiero decir a fondo! ), Por lo que buscar estos bucles homogéneos te proporciona áreas aisladas de la base de código sobre las que puedes razonar fácilmente en términos de los efectos secundarios hasta el punto en que puede paralelistar con confianza y seguridad esos puntos de acceso. También mejorará la mantenibilidad del código haciendo que sea muy fácil razonar acerca de los cambios de estado que se producen en esa pieza de código en particular. Guarde el sueño de la aplicación uber multiproceso ejecutando todo en paralelo para más adelante. Por ahora, concéntrese en identificar / extraer ciclos homogéneos críticos para el rendimiento con flujos de control simples y efectos secundarios simples. Esos son sus objetivos prioritarios para la paralelización con bucles paralelizados simples.
Ahora admití que de alguna manera eludí tus preguntas, pero la mayoría de ellas no necesitan aplicarse si haces lo que sugiero, al menos hasta que hayas trabajado hasta llegar al punto en el que piensas más en diseños de subprocesos múltiples que en el opuesto. simplemente paralelizar los detalles de implementación. Y es posible que ni siquiera necesite ir tan lejos para tener un producto muy competitivo en términos de rendimiento. Si tiene que trabajar mucho en un solo bucle, puede dedicar los recursos de hardware a hacer que ese bucle sea más rápido en lugar de hacer que muchas operaciones se ejecuten simultáneamente. Si tiene que recurrir a más métodos asíncronos, como si sus puntos de acceso están más vinculados con E / S, busque un enfoque asincrónico / de espera en el que desencadene una tarea asíncrona, pero haga algunas cosas mientras tanto y luego espere las tareas asíncronas. completar. Incluso si eso no es absolutamente necesario, la idea es dividir las áreas aisladas de su base de código donde pueda, con un 100% de confianza (o al menos 99.9999999%) decir que el código multiproceso es correcto.
No querrás jugar con las condiciones de carrera. No hay nada más desmoralizador que encontrar una oscura condición de carrera que solo ocurre una vez en luna llena en alguna máquina de un usuario aleatorio mientras que su equipo de control de calidad completo no puede reproducirla, solo 3 meses después se encuentra con usted mismo, excepto durante ese tiempo ejecutó una compilación de lanzamiento sin información de depuración disponible mientras usted se da vuelta y se duerme sabiendo que su base de código puede escamarse en cualquier momento dado, pero de forma que nadie nunca podrá reproducirse de manera consistente. Así que tómalo con sencillez con las bases de código heredadas de subprocesamiento múltiple, al menos por ahora, y adhiérete a secciones de subprocesamiento aisladas pero críticas de la base de código donde los efectos secundarios son absolutamente simples de razonar. Y pruebe todo: idealmente, aplique un enfoque TDD donde escriba una prueba para el código que va a procesar varias veces para asegurarse de que da la salida correcta después de que termine ... aunque las condiciones de carrera son el tipo de cosas que volar fácilmente bajo el radar de la unidad y las pruebas de integración, por lo que, de nuevo, es absolutamente necesario que pueda comprender la totalidad de los efectos secundarios que ocurren en un código determinado antes de tratar de leerlos varias veces. La mejor manera de hacerlo es hacer que los efectos secundarios sean tan fáciles de comprender como sea posible con los flujos de control más simples que causan un solo tipo de efecto secundario para todo un ciclo.
Parece que tiene varios problemas diferentes que el paralelismo puede abordar, pero de diferentes maneras.
El rendimiento aumenta mediante la utilización de CPU de varios núcleos Architecutres
No está aprovechando los archivadores de CPU de múltiples núcleos que se están volviendo tan comunes. La paralelización le permite dividir el trabajo entre múltiples núcleos. Puede escribir ese código a través de las técnicas estándar de división y conquista de C ++ utilizando un estilo de programación "funcional" donde pasa el trabajo para separar los hilos en la etapa de división. El patrón MapReduce de Google es un ejemplo de esa técnica. Intel tiene la nueva biblioteca CILK para brindarle compatibilidad con el compilador C ++ para tales técnicas.
Mayor capacidad de respuesta de la GUI a través de una vista asíncrona de documentos
Al separar las operaciones de la GUI de las operaciones del documento y colocarlas en diferentes hilos, puede aumentar la capacidad de respuesta aparente de su aplicación. Los patrones de diseño estándar de Model-View-Controller o Model-View-Presenter son un buen lugar para comenzar. Debe paralelizarlos haciendo que el modelo informe la vista de las actualizaciones en lugar de dejar que la vista proporcione el hilo sobre el que se calcula el documento. La vista llamaría a un método en el modelo pidiéndole que calcule una vista particular de los datos, y el modelo informaría al presentador / controlador a medida que se cambia información o nuevos datos disponibles, que pasarían a la vista para actualizarse.
Caché y precálculo oportunistas Parece que su aplicación tiene una base de datos fija, pero muchas vistas computacionales intensivas sobre los datos. Si realizó un análisis estadístico sobre qué vistas se solicitaban más comúnmente en qué situaciones, podría crear subprocesos de trabajador de fondo para precalcular los valores solicitados probables. Puede ser útil poner estas operaciones en subprocesos de baja prioridad para que no interfieran con el procesamiento de la aplicación principal.
Obviamente, necesitará usar mutexes (o secciones críticas), eventos y probablemente semáforos para implementar esto. Puede encontrar útiles algunos de los nuevos objetos de sincronización en Vista, como el bloqueo delgado de lector-escritor, las variables de condición o la nueva API del grupo de subprocesos. Consulte el libro de Joe Duffy sobre concurrencia para saber cómo usar estas técnicas básicas.
Puede comenzar rompiendo la interfaz de usuario y la tarea de trabajo en hilos separados.
En su método de pintura en lugar de llamar a getData () directamente, coloca la solicitud en una cola segura para subprocesos. getData () se ejecuta en otro hilo que lee sus datos de la cola. Cuando se completa el subproceso getData, señala el hilo principal para volver a dibujar el área de visualización con sus datos de resultado mediante la sincronización de hilos para pasar los datos.
Mientras todo esto sucede, por supuesto, tienes una barra de progreso que dice "reticulando splines" para que el usuario sepa que algo está pasando.
Esto mantendría su interfaz de usuario ágil sin el dolor significativo de multirreprocesamiento de sus rutinas de trabajo (que puede ser similar a una reescritura total)
Si fuera mi dinero de desarrollo el que estaba gastando, comenzaría con el panorama general:
¿Qué espero lograr y cuánto gastaré para lograr esto, y cómo estaré más adelante? (Si la respuesta es que mi aplicación funcionará un 10% mejor en PC quadcore, y podría haber logrado el mismo resultado al gastar $ 1000 más por PC de cliente, y gastar $ 100,000 menos este año en I + D, entonces, me saltaría el todo el esfuerzo).
¿Por qué estoy haciendo múltiples subprocesos en lugar de distribución masivamente paralela? ¿Realmente creo que los hilos son mejores que los procesos? Los sistemas multi-core también ejecutan aplicaciones distribuidas bastante bien. Y hay algunas ventajas en los sistemas basados en procesos de paso de mensajes que van más allá de los beneficios (¡y los costos!) Del enhebrado. ¿Debería considerar un enfoque basado en procesos? ¿Debería considerar un fondo que se ejecute completamente como un servicio y una GUI de primer plano? Como mi producto está bloqueado por nodos y con licencia, creo que los servicios se adaptarán bastante bien a mí (proveedor). Además, separar las cosas en dos procesos (servicio en segundo plano y primer plano) podría forzar el tipo de reescritura y restauración que puede que no se vea forzado a hacer, si tuviera que agregar subprocesos en mi mezcla.
Esto es solo para que piense: ¿qué pasaría si lo reescribiera como un servicio (aplicación en segundo plano) y una GUI, porque eso sería más fácil que agregar subprocesos, sin agregar bloqueos, interbloqueos y condiciones de carrera?
Considere la idea de que para sus necesidades, quizás enhebrar es malo. Desarrolla tu religión y sigue con eso. A menos que tengas una buena razón para ir por el otro camino. Durante muchos años, evité religiosamente el enhebrado. Porque un hilo por proceso es lo suficientemente bueno para mí.
No veo razones realmente sólidas en su lista por las que deba enhebrar, excepto aquellas que podrían ser resueltas de manera menos costosa por hardware más costoso. Si tu aplicación es "demasiado lenta", agregar hilos podría incluso no acelerarla.
Utilizo hilos para comunicaciones seriales de fondo, pero no consideraría enhebrar meras aplicaciones computacionalmente pesadas, a menos que mis algoritmos fueran tan intrínsecamente paralelos como para aclarar los beneficios, y los inconvenientes sean mínimos.
Me pregunto si los problemas de "diseño" que tiene esta aplicación C ++ Builder son como mi enfermedad de aplicación Delphi "RAD Spaghetti". Descubrí que una refacturación / reescritura mayorista (más de un año por cada aplicación principal a la que le he hecho esto), fue una cantidad de tiempo mínima para que pudiera manejar la "complejidad accidental" de la aplicación. Y eso fue sin tirar una idea de "hilos donde sea posible". Tiendo a escribir mis aplicaciones solo con subprocesos para comunicación serial y manejo de socket de red. Y tal vez el extraño "hilo de trabajo-cola".
Si hay un lugar en su aplicación puede agregar UN hilo, para probar las aguas, buscaría la "cola de trabajos" principal y crearía una rama experimental de control de versiones, y aprendería cómo funciona mi código al romper en la rama experimental. Agrega ese hilo. Y mira dónde pasas el primer día de depuración. Entonces, podría abandonar esa rama y regresar a mi tronco hasta que desaparezca el dolor en mi lóbulo temporal.
Madriguera
También puede consultar este artículo de Herb Sutter . Tiene una gran cantidad de código existente y desea agregar simultaneidad. ¿Dónde comienzas?
Tienes un gran desafío por delante. Tenía un reto similar: una base de código monolítica de 15 años, de un solo hilo, sin aprovechar las multinúcleo, etc. Dedicamos un gran esfuerzo a tratar de encontrar un diseño y una solución que funcionaran y funcionaran.
Malas noticias primero. Será algo entre poco práctico e imposible hacer que su aplicación de subproceso único sea multiproceso. Una aplicación de una sola hebra se basa en su carácter único, es sutil y grosera. Un ejemplo es si la parte de cálculo requiere entrada de la parte de la GUI. La GUI debe ejecutarse en el hilo principal. Si intentas obtener estos datos directamente del motor de cómputo, es probable que te encuentres en un punto muerto y en condiciones de carrera que requieran rediseños importantes para solucionarlos. Muchas de estas confianzas no surgirán durante la fase de diseño, o incluso durante la fase de desarrollo, sino solo después de que una compilación de lanzamiento se ponga en un entorno hostil.
Más malas noticias La programación de aplicaciones multiproceso es excepcionalmente difícil. Puede parecer bastante sencillo simplemente bloquear cosas y hacer lo que tienes que hacer, pero no es así. En primer lugar, si bloquea todo a la vista, terminará serializando su aplicación, anulando todos los beneficios de mutthreading, al mismo tiempo que agrega toda la complejidad. Incluso si vas más allá de esto, escribir una aplicación de MP sin defectos es bastante difícil, pero escribir una aplicación MP de alto rendimiento es mucho más difícil. Podrías aprender en el trabajo en una especie de bautismal por fuego. Pero si está haciendo esto con el código de producción, especialmente el código de producción heredado , pone en riesgo su negocio.
Ahora las buenas noticias. Tienes opciones que no implican la refacturación de toda tu aplicación y te darán la mayoría de lo que buscas. Una opción en particular es fácil de implementar (en términos relativos) y mucho menos propensa a defectos que hacer que su aplicación sea completamente MP.
Puede crear instancias de copias múltiples de su aplicación. Haz que uno de ellos sea visible y todos los demás invisibles. Use la aplicación visible como la capa de presentación, pero no realice el trabajo computacional allí. En su lugar, envíe mensajes (tal vez a través de sockets) a las copias invisibles de su aplicación que hacen el trabajo y envían los resultados a la capa de presentación.
Esto puede parecer un truco. Y tal vez lo es. Pero obtendrá lo que necesita sin poner en riesgo la estabilidad y el rendimiento de su sistema. Además, hay beneficios ocultos. Una es que las copias invisibles del motor de su aplicación tendrán acceso a su propio espacio de memoria virtual, lo que facilita el aprovechamiento de todos los recursos del sistema. También escala muy bien. Si está ejecutando en un cuadro de 2 núcleos, puede separar 2 copias de su motor. 32 núcleos? 32 copias. Entiendes la idea.