¿Consejos para convertir una gran aplicación monolítica de un solo hilo en una architecture multiproceso?

El principal producto de mi empresa es una gran aplicación monolítica de C ++, utilizada para el procesamiento y visualización de datos científicos. Su código base se remonta a 12 o 13 años, y aunque hemos trabajado para actualizarlo y mantenerlo (el uso de STL y Boost, cuando me uní a la mayoría de los contenedores fue personalizado, por ejemplo, actualizado completamente a Unicode y al VCL 2010, etc.) Queda un problema muy importante que queda: está completamente centrado. Dado que es un progtwig de visualización y procesamiento de datos, esto se está convirtiendo cada vez más en un obstáculo.

Soy desarrollador y gerente de proyectos para la próxima versión en la que queremos abordar esto, y este 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 progtwig 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 se calculará o leerá desde un archivo o cualquier otra cosa que se requiera (a menudo es 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 caché, esto puede llevar mucho tiempo. A veces esto es minutos. Se producen rutas similares para otras partes del progtwig que realizan operaciones de procesamiento prolongadas; el progtwig no responde durante todo el tiempo, a veces horas.

Estoy buscando consejos sobre cómo abordar el cambio de esto. Ideas prácticas. Quizás cosas como:

  • ¿Diseña patrones para solicitar datos de forma asíncrona?
  • ¿Almacenar grandes colecciones de objetos para que los hilos puedan leer y escribir de forma segura?
  • ¿Cómo manejar la invalidación de conjuntos de datos mientras algo intenta leerlo?
  • ¿Existen patrones y técnicas para este tipo de problema?
  • ¿Qué debería preguntarme si no he pensado?

No he hecho ninguna progtwigción multiproceso desde mis días en Uni hace unos años, y creo que el rest de mi equipo está en una posición similar. Lo que sabía era académico, no práctico, y no está lo suficientemente cerca como para que la confianza se acerque a esto.

El objective final es tener un progtwig totalmente sensible, donde todos los cálculos y la generación de datos se realicen en otros subprocesos y la interfaz de usuario siempre responda. Podríamos no llegar allí en un solo ciclo de desarrollo 🙂


Edición: 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 a las bibliotecas paralelas que podemos usar, ya que la mayoría parece (?) Estar escritas solo para GCC o MSVC. Afortunadamente, lo están desarrollando activamente y su compatibilidad con los estándares C ++ es mucho mejor de lo que solía ser. El comstackdor soporta estos componentes Boost .
  • Su architecture no es tan limpia como debería ser y los componentes a menudo están demasiado apretados. Este es otro problema 🙂

Edit # 2: Gracias por las respuestas hasta ahora!

  • Me sorprende que tantas personas hayan recomendado una architecture multiproceso (es la respuesta más votada en este momento), no multiproceso. Mi impresión es que es una estructura de progtwig muy Unix-ish, y no sé nada acerca de cómo está diseñado 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 subprocesos múltiples, ¿existen patrones de diseño para la solicitud asincrónica y el consumo de datos, o sistemas de MVP asíncronos o asíncronos, o cómo diseñar un sistema orientado a tareas, artículos y libros y deconstrucciones posteriores al lanzamiento? ¿Ilustrando cosas que funcionan y cosas que no funcionan? Por supuesto, podemos desarrollar toda esta architecture nosotros mismos, pero es bueno trabajar a partir de lo que otros han hecho antes y saber qué errores y trampas evitar.
  • Un aspecto que no se menciona en ninguna respuesta es el manejo de este proyecto. Mi impresión es estimar cuánto tiempo tomará esto y mantener un buen control del proyecto cuando se hace algo tan incierto como puede ser difícil. Esa es una razón por la que busco recetas o consejos prácticos de encoding, supongo, para guiar y restringir la dirección de encoding tanto como sea posible.

Todavía no he marcado una respuesta para esta pregunta, esto no se debe a la calidad de las respuestas, lo cual es genial (y muchas gracias), sino simplemente porque, debido al scope de esto, espero más respuestas o discusión. ¡Gracias a los que ya han respondido!

Por lo tanto, 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 prácticamente sin cambios, excepto envueltos en un subproceso que recoge elementos de trabajo de una cola y deposita los resultados en uno. Podría ir un paso más allá y usar sockets y procesos en lugar de colas y subprocesos; esto le permitirá repartirse en múltiples máquinas si hay un beneficio de 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 clases, como sugiere su descripción, entonces esta podría ser la forma más sencilla de paralelizarlo.

Tienes un gran reto por delante. Tenía un desafío similar por delante: una base de código monolítico de 15 años de un solo hilo, sin aprovechar el multinúcleo, etc. Hicimos un gran esfuerzo para encontrar un diseño y una solución que fuera viable y funcionara.

Malas noticias primero. Estará en algún lugar entre lo poco práctico e imposible de hacer que su aplicación de un solo hilo sea multihilo. Una aplicación de un solo subproceso se basa en su sencillez, así como en formas sutiles y groseras. Un ejemplo es si la parte de cálculo requiere una entrada de la parte GUI. La GUI debe ejecutarse en el hilo principal. Si intenta obtener estos datos directamente del motor de cómputo, probablemente se encontrará con condiciones de interlocking y de carrera que requerirán rediseños importantes para corregirlos. Muchas de estas dependencias no surgirán durante la fase de diseño, ni siquiera durante la fase de desarrollo, sino solo después de que una versión de lanzamiento se coloque en un entorno hostil.

Más malas noticias. Progtwigr aplicaciones multihilo es excepcionalmente difícil. Puede parecer bastante sencillo bloquear cosas y hacer lo que tienes que hacer, pero no lo es. En primer lugar, si bloquea todo lo que está a la vista, termina por serializar su aplicación, negando todos los beneficios del mutithreading en primer lugar y al mismo tiempo añadiendo toda la complejidad. Incluso si va más allá de esto, escribir una aplicación MP sin defectos es lo suficientemente 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 bautismo por fuego. Pero si está haciendo esto con el código de producción, especialmente el código de producción heredado , se pone en riesgo su negocio.

Ahora las buenas noticias. Tiene opciones que no implican refactorizar toda su aplicación y le dará la mayor parte de lo que busca. 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 totalmente MP.

Podrías instanciar múltiples copias de tu aplicación. Haz uno de ellos visible, y todos los demás invisibles. Use la aplicación visible como la capa de presentación, pero no haga el trabajo computacional allí. En su lugar, envíe mensajes (quizás a través de sockets) a las copias invisibles de su aplicación que hacen el trabajo y envíe los resultados a la capa de presentación.

Esto puede parecer un hack. Y tal vez lo sea. Pero le proporcionará 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 se escala muy bien. Si está ejecutando en una caja de 2 núcleos, podría apagar 2 copias de su motor. 32 núcleos? 32 ejemplares. Tienes la idea

  1. No intente multihilo todo en la aplicación antigua. El subprocesamiento múltiple por el simple hecho de decir que es multiproceso es una pérdida de tiempo y dinero. Estás creando una aplicación que hace algo, no un monumento para ti.
  2. Perfile y estudie sus flujos de ejecución para averiguar dónde pasa la aplicación la mayor parte de su tiempo. Un generador de perfiles es una gran herramienta para esto, pero también está recorriendo el código en el depurador. Encuentras las cosas más interesantes en paseos al azar.
  3. Desacoplar la interfaz de usuario de los cálculos de larga duración. Use técnicas de comunicación entre hilos para enviar actualizaciones a la interfaz de usuario desde el hilo de cómputo.
  4. Como un efecto secundario de # 3: piense cuidadosamente acerca de la reentrada: ahora que el cálculo se ejecuta en segundo plano y el usuario puede hacer polémica en la interfaz de usuario, ¿qué cosas en la interfaz de usuario deberían estar desactivadas para evitar conflictos con la operación en segundo plano? Permitir que el usuario elimine un conjunto de datos mientras se está ejecutando un cálculo sobre esos datos es probablemente una mala idea. (Mitigación: el cálculo hace una instantánea local de los datos) ¿Tiene sentido para el usuario poner en cola varias operaciones de cómputo simultáneamente? Si se maneja bien, esta podría ser una nueva característica y ayudar a racionalizar el esfuerzo de retrabajo de la aplicación. Si se ignora, será un desastre.
  5. Identifique operaciones específicas que sean candidatas para ser insertadas en un hilo de fondo. El candidato ideal suele ser una sola función o clase que realiza mucho trabajo (requiere “mucho tiempo” para completarse, más de unos pocos segundos) con entradas y salidas bien definidas, que no utiliza recursos globales y No toque la interfaz de usuario directamente. Evalúe y priorice a los candidatos según la cantidad de trabajo que se requeriría para adaptarse a este ideal.
  6. En términos de gestión de proyectos, tome las cosas paso a paso. Si tiene varias operaciones que son candidatos fuertes para ser movidas a un hilo de fondo y no tienen interacción entre ellas, estas podrían implementarse en paralelo por varios desarrolladores. Sin embargo, sería un buen ejercicio hacer que todos participen primero en una conversión para que todos entiendan qué buscar y establecer sus patrones para la interacción de la interfaz de usuario, etc. Organice una reunión de pizarra extendida para hablar sobre el diseño y el proceso de extracción de la unidad. función en un hilo de fondo. Ir a implementar eso (juntos o repartir piezas a individuos), luego volver a reunirse para poner todo junto y discutir los descubrimientos y los puntos de dolor.
  7. El subprocesamiento múltiple es un dolor de cabeza y requiere una reflexión más cuidadosa que la encoding directa, pero dividir la aplicación en múltiples procesos crea muchos más dolores de cabeza, OMI. El soporte de subprocesos y los primitivos disponibles son buenos en Windows, quizás mejores que otras plataformas. Usalos, usalos a ellos.
  8. En general, no hagas más de lo que se necesita. Es fácil implementar demasiado y complicar un problema al lanzar más patrones y bibliotecas estándar.
  9. Si nadie en su equipo ha realizado anteriormente trabajos de subprocesos múltiples, dedique tiempo para que un experto o fondos contraten uno como consultor.

Lo principal que debes hacer es desconectar tu IU de tu conjunto de datos. Yo sugeriría que la forma de hacerlo es poner una capa en medio.

Deberá diseñar una estructura de datos de datos cocinados para la visualización. Lo más probable es que contenga copias de algunos de sus datos de back-end, pero “cocinado” sea fácil de extraer. La idea clave aquí es que esto es rápido y fácil de pintar. Incluso puede hacer que esta estructura de datos contenga posiciones de pantalla calculadas de bits de datos para que sea rápido de dibujar.

Siempre que reciba un mensaje WM_PAINT, deberá obtener la versión completa más reciente de esta estructura y extraer de ella. Si haces esto correctamente, deberías poder manejar múltiples mensajes WM_PAINT por segundo porque el código de pintura nunca se refiere a los datos de tu back-end. Solo está girando a través de la estructura cocida. La idea aquí es que es mejor pintar datos obsoletos rápidamente que colgar su interfaz de usuario.

Mientras tanto…

Debe tener 2 copias completas de esta estructura preparada para la exhibición. Uno es el aspecto del mensaje WM_PAINT. ( llámelo cfd_A ) El otro es lo que le entrega a su función CookDataForDisplay (). (llámalo cfd_B ). Su función CookDataForDisplay () se ejecuta en un subproceso separado y funciona en la creación / actualización de cfd_B en segundo plano. Esta función puede durar todo el tiempo que desee, ya que no interactúa con la pantalla de ninguna manera. Una vez que se devuelva la llamada, 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 preparada para la visualización sea un bitmap, y esa podría ser una buena manera de comenzar a rodar la pelota, pero estoy seguro de que con un poco de pensamiento puede hacer mucho. Mejor trabajo con una estructura más sofisticada.

Entonces, volviendo a su 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 la interfaz de usuario. Mientras tanto, cfd_B se está construyendo mediante un subproceso en segundo plano utilizando llamadas GetData.

La manera rápida y sucia de hacer esto es

  1. Tome su código WM_PAINT actual, péguelo en una función llamada PaintIntoBitmap ().
  2. Cree un bitmap y una memoria DC, esto es cfd_B.
  3. Crea un hilo y pásalo cfd_B y haz que llame a PaintIntoBitmap ()
  4. Cuando este hilo se complete, intercambie cfd_B y cfd_A

Ahora su nuevo método WM_PAINT solo toma el bitmap pre-renderizado en cfd_A y lo dibuja en la pantalla. Su interfaz de usuario ahora está desconectada de la función GetData () del servidor.

Ahora comienza el verdadero trabajo, porque la forma rápida y sucia no maneja muy bien el tamaño de la ventana. Puede ir desde allí para refinar las estructuras de cfd_A y cfd_B poco a poco hasta llegar a un punto en el que esté satisfecho con el resultado.

Puede que empiece a dividir la IU 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 realiza el subproceso getData, señala el subproceso principal para volver a dibujar el área de visualización con sus datos de resultados utilizando la sincronización de subprocesos para pasar los datos.

Mientras todo esto sucede, tienes una barra de progreso que dice reticulación de splines para que el usuario sepa que algo está sucediendo.

Esto mantendría su interfaz de usuario ágil sin el dolor significativo de multihilo en sus rutinas de trabajo (lo que puede ser similar a una reescritura total)

Parece que tienes varios problemas diferentes que el paralelismo puede abordar, pero de diferentes maneras.

El rendimiento aumenta a través de la utilización de CPU Architecutres multinúcleo

No estás aprovechando las architectures 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 “funcional” de progtwigción en el que pasa el trabajo a hilos separados 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 comstackdor C ++ para tales técnicas.

Mayor capacidad de respuesta de la GUI a través de la vista de documentos asíncrona

Al separar las operaciones de la GUI de las operaciones del documento y ubicarlas en diferentes hilos, puede boost la capacidad de respuesta aparente de su aplicación. Los patrones de diseño estándar Model-View-Controller o Model-View-Presenter son un buen lugar para comenzar. Debe paralelizarlos haciendo que el modelo informe a la vista de las actualizaciones en lugar de que la vista proporcione el hilo en el que el documento se computa. La Vista llamaría a un método en el modelo que le pedía que computara una vista particular de los datos, y el modelo informaría al presentador / controlador a medida que se cambia la información o que hay nuevos datos disponibles, que se pasarían a la vista para actualizarse.

Almacenamiento en caché oportunista y cálculo previo Parece que su aplicación tiene una base de datos fija, pero muchas vistas posibles de uso intensivo de cómputo en los datos. Si realizó un análisis estadístico sobre qué vistas se solicitaron con mayor frecuencia en qué situaciones, podría crear subprocesos de trabajo en segundo plano para calcular previamente los valores probables. Puede ser útil colocar estas operaciones en subprocesos de baja prioridad para que no interfieran con el procesamiento principal de la aplicación.

Obviamente, necesitará usar mutexes (o secciones críticas), eventos y probablemente semáforos para implementar esto. Es posible que algunos de los nuevos objetos de sincronización en Vista sean útiles, como el locking delgado de lector-escritor, las variables de condición o la nueva API de grupo de subprocesos. Vea el libro de Joe Duffy sobre la concurrencia para saber cómo usar estas técnicas básicas.

Hay algo de lo que nadie ha hablado todavía, pero que es bastante interesante.

Se llama future s. Un futuro es la promesa de un resultado … veamos con un ejemplo.

 future leftVal = computeLeftValue(treeNode); // [1] int rightVal = computeRightValue(treeNode); // [2] result = leftVal + rightVal; // [3] 

Es bastante simple:

  1. Se apaga un hilo que comienza a calcular leftVal , tomándolo de un grupo, por ejemplo, para evitar el problema de inicialización.

  2. Mientras se calcula rightVal , se calcula rightVal .

  3. Agrega los dos, esto puede bloquearse si leftVal aún no se ha calculado y esperar a que finalice 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 las future , estarán disponibles en el próximo C++0x pero ya hay bibliotecas disponibles hoy en día, aunque la syntax quizás no sea tan bonita como le haría creer;)

Si fueran los dólares de desarrollo que estaba gastando, comenzaría con el outlook general:

  1. ¿Qué espero lograr, y cuánto gastaré para lograr esto, y cómo estaré más adelante? (Si la respuesta a esto es, mi aplicación se ejecutará un 10% mejor en las computadoras de cuatro puntos, y podría haber logrado el mismo resultado al gastar $ 1000 más por computadora del cliente, y gastar $ 100,000 menos este año en investigación y desarrollo, entonces, me saltearía la todo el esfuerzo).

  2. ¿Por qué hago multiproceso en lugar de distribución masiva en paralelo? ¿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) de subprocesos. ¿Debo considerar un enfoque basado en procesos? ¿Debo considerar un fondo que se ejecuta completamente como un servicio y una interfaz gráfica de usuario en primer plano? Dado que mi producto está bloqueado por nodo y con licencia, creo que los servicios se adaptarían bastante bien a mí (proveedor). Además, separar cosas en dos procesos (servicio en segundo plano y primer plano) podría forzar el tipo de reescritura y rearchitecting para que no me vieran obligado a hacerlo, si fuera a agregar hilos a mi mezcla.

  3. Esto es solo para que piense: ¿Qué pasaría si lo reescribiera como un servicio (aplicación de fondo) y una GUI, porque eso sería más fácil que agregar subprocesos, sin agregar lockings, puntos muertos y condiciones de carrera?

  4. Considera la idea de que para tus necesidades, tal vez el enhebrado sea malo. Desarrolla tu religión y quédate con eso. A menos que tengas una buena razón para ir por el otro lado. Durante muchos años, religiosamente evité el enhebrado. Porque un hilo por proceso es lo suficientemente bueno para mí.

No veo ninguna razón realmente sólida en su lista por la que necesite subprocesos, excepto los que podrían resolverse de forma más económica con un hardware de computadora de destino más costoso. Si su aplicación es “demasiado lenta”, la adición de subprocesos podría ni siquiera acelerarla.

Utilizo subprocesos para comunicaciones seriales en segundo plano, pero no lo consideraría solo para aplicaciones pesadas computacionalmente, a menos que mis algoritmos fueran tan 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 Delphi “RAD Spaghetti”. Descubrí que un refactor / reescritura al por mayor (más de un año por cada aplicación importante a la que he hecho esto) fue un tiempo mínimo para que yo pudiera manejar la “complejidad accidental” de la aplicación. Y eso fue sin lanzar una idea de “hilos donde sea posible”. Tiendo a escribir mis aplicaciones con subprocesos para comunicación en serie y manejo de socket de red, solo. Y tal vez la extraña “cola de subproceso de trabajo”.

Si hay un lugar en su aplicación, puede agregar UN subproceso para probar las aguas, buscaría la “cola de trabajo” principal y crearía una twig de control de versión experimental, y aprendería cómo funciona mi código rompiendo En la twig experimental. Añade ese hilo. Y ver dónde pasas tu primer día de depuración. Entonces podría abandonar esa twig y regresar a mi tronco hasta que el dolor en mi lóbulo temporal desaparezca.

Madriguera

Esto es lo que yo haría …

Empezaría perfilando tu y viendo:

1) lo que es lento y cuáles son las rutas activas 2) qué llamadas son reentrantes o profundamente anidadas

puede usar 1) para determinar dónde está la oportunidad de acelerar y dónde comenzar a buscar la paralelización.

puede usar 2) para averiguar dónde es probable que se encuentre el estado compartido y tener una idea más profunda de cuánto se enredan las cosas.

Yo usaría un buen generador de perfiles del sistema y un buen generador de perfiles de muestreo (como el kit de herramientas para el rendimiento de Windows o las vistas de concurrencia del generador de perfiles en Visual Studio 2010 Beta2, ambas son ‘gratuitas’ en este momento).

Luego, descubriría cuál es el objective y cómo separar las cosas gradualmente para lograr un diseño más limpio que responda mejor (moviendo el trabajo del subproceso de la interfaz de usuario) y tenga un mejor rendimiento (paralelización de las partes intensivas en computación). Me concentraría primero en la prioridad más alta y en los elementos más notables.

Si no tiene una buena herramienta de refactorización como VisualAssist, invierta en una, vale la pena. Si no está familiarizado con los libros de refactorización de Michael Feathers o Kent Beck, considere pedirlos prestados. Me aseguraría de que mis refactorizaciones estén bien cubiertas por pruebas unitarias.

No puedes moverte a VS (recomendaría los productos que trabajo en la biblioteca de agentes asíncronos y en la biblioteca de patrones paralelos, también puedes usar TBB o OpenMP).

En boost, observaría con cuidado boost :: thread, la biblioteca asio y la biblioteca de señales.

Pediría ayuda / guía / un oído atento cuando me quedé atascado.

-Almiar

También puede consultar este artículo de Herb Sutter . Tiene una masa de código existente y desea agregar concurrencia. Por donde empiezas

Bueno, creo que estás esperando mucho en base a tus comentarios aquí. No va a pasar de minutos a milisegundos mediante subprocesos múltiples. Lo más que puede esperar es la cantidad de tiempo actual dividida por la cantidad de núcleos. Dicho esto, estás de suerte con C ++. He escrito aplicaciones científicas multiprocesador de alto rendimiento, y lo que quieres buscar es el bucle más vergonzosamente paralelo que puedas encontrar. En mi código científico, la pieza más pesada está calculando entre 100 y 1000 puntos de datos. Sin embargo, todos los puntos de datos se pueden calcular independientemente de los otros. A continuación, puede dividir el bucle utilizando openmp. Esta es la manera más fácil y eficiente de ir. Si su comstackdor no es compatible con openmp, le será muy difícil transferir el código existente. Con openmp (si tiene suerte), es posible que solo tenga que agregar un par de #pragmas para obtener 4-8x el rendimiento. Aquí hay un ejemplo de StochFit

Espero que esto te ayude a comprender y convertir tu aplicación monolítica de un solo hilo a múltiples hilos fácilmente. Lo siento, es para otro lenguaje de progtwigción, pero no obstante, los principios explicados son los mismos en todas partes.

http://www.freevbcode.com/ShowCode.Asp?ID=1287

Espero que esto ayude.

Lo primero que debes hacer es separar tu GUI de tus datos, lo segundo es crear una clase de multiproceso.

PASO 1 – GUI sensible

Podemos asumir que la imagen que está produciendo está contenida en el canvas de un TImage. Puede poner un TTimer simple en su formulario y puede 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 es simple. El punto es que:

  1. Necesita un objeto que se crea en el hilo principal
  2. El objeto se copia en el formulario que necesita, solo cuando es necesario y de manera segura (está bien, puede ser necesaria una mejor protección para el bitmap, pero para la simplicidad …)
  3. El objeto CurrentData es su proyecto real, de un solo hilo, que produce una imagen.

Ahora tienes una GUI rápida y receptiva. Si su algoritmo es lento, la actualización es lenta, pero su usuario nunca pensará que su progtwig está congelado.

PASO 2 – Multihilo

Te sugiero que implementes 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 su clase de subproceso simple, puede crear un objeto como este:

 TSimpleThread *ST; ST=new TSimpleThread( RefreshFunction,NULL,true); ST->Resume(); 

Explicemos mejor: ahora, en tu propia clase monolítica, has creado un hilo. Más: traes una función (es decir, RefreshFunction) en un hilo separado . El scope de su función es el mismo, la clase es la misma, la ejecución es separada.

Mi sugerencia número uno, aunque es muy tarde (perdón por revivir el hilo anterior, ¡es interesante!) Es buscar bucles de transformación homogéneos en los que cada iteración del bucle esté mutando un dato completamente independiente de las otras iteraciones.

En lugar de pensar en cómo convertir este antiguo código en una base asíncrona, se ejecutan todo tipo de operaciones en paralelo (lo que podría estar pidiendo todo tipo de problemas, peor que el rendimiento de un solo hilo de patrones de locking deficientes o exponencialmente peor, condiciones de carrera / puntos muertos) al tratar de hacer esto en retrospectiva al código que no puede comprender por completo), apéguese a la mentalidad secuencial para el diseño general de la aplicación por ahora, pero identifique o extraiga bucles de transformación simples y homogéneos. No pase de los subprocesos múltiples de nivel de diseño amplio e intrusivo e intente profundizar en los detalles. Trabaje primero con subprocesos múltiples no intrusivos de detalles finos de implementación 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 sencilla, como:

 for each pixel in image: make it brighter 

Es muy fácil razonar y puede paralelizar este bucle de forma segura sin ningún problema utilizando OMP o TBB o lo que sea y sin enredarse en la sincronización de subprocesos. Solo toma 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énea simple y si tiene bucles complejos que actualizan muchos tipos diferentes de datos con flujos de control complejos que desencadenan efectos secundarios complejos, luego busque refactorizar 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, cada uno de los cuales desencadena solo un tipo de efecto secundario para un tipo de datos con un flujo de control más simple. Hacer varios bucles en lugar de uno puede parecer un poco inútil, pero los bucles se vuelven más simples, la homogeneidad a menudo conduce a patrones de acceso secuencial de memoria más fáciles de usar caché en comparación con los patrones esporádicos de acceso aleatorio, y luego tiende a encontrar muchas más oportunidades para paralelice con seguridad (y vectorice) el código de una manera directa.

Primero, debe comprender a fondo los efectos secundarios de cualquier código que intente paralelizar (y me refiero a los detalles), por lo que buscar estos bucles homogéneos le brinda áreas aisladas de la base de código que puede razonar fácilmente en términos de los efectos secundarios. hasta el punto en que pueda paralelizar con confianza y seguridad esos puntos de acceso. También mejorará la capacidad de mantenimiento del código al hacer que sea muy fácil razonar acerca de los cambios de estado que ocurren en esa pieza particular de código. Guarde el sueño de la aplicación de multiproceso súper ejecutando todo en paralelo para más adelante. Por ahora, enfóquese en identificar / extraer bucles homogéneos, críticos para el rendimiento, con flujos de control simples y efectos secundarios simples. Esos son sus objectives prioritarios para la paralelización con simples bucles paralelizados.

Admito que de alguna manera esquivé tus preguntas, pero la mayoría de ellas no tienen que aplicarse si haces lo que sugiero, al menos hasta que hayas llegado al punto en el que estás pensando más en diseños de subprocesos múltiples como opuestos. simplemente paralelizando 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 mucho trabajo que hacer en un solo bucle, puede dedicar los recursos de hardware para hacer que ese bucle vaya 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 a la E / S, busque un enfoque asíncrono / espera en el que dispare una tarea asíncrona pero haga algunas cosas mientras tanto y luego espere en la tarea (s) asíncrona completar. Incluso si eso no es absolutamente necesario, la idea es separar áreas aisladas de su base de código donde pueda, con 100% de confianza (o al menos 99.9999999%) decir que el código de multiproceso es correcto.

Nunca querrás apostar en condiciones de carrera. No hay nada más desmoralizador que encontrar alguna condición de raza oscura que solo ocurre una vez en la luna llena en la máquina de algún usuario aleatorio, mientras que todo su equipo de control de calidad no puede reproducirla, solo que, 3 meses después, la encuentra usted mismo, excepto durante ese tiempo. ejecutó una versión de lanzamiento sin la información de depuración disponible, mientras que luego tira y vuelve a dormir sabiendo que su base de código puede explotar en un momento dado, pero de una manera que nadie podrá reproducir constantemente. Así que tómelo con calma con las bases de código heredadas de subprocesos múltiples, al menos por ahora, y apéguese a las secciones críticas pero múltiples de subprocesos de la base de códigos donde los efectos secundarios son muy simples de entender. Y haga una prueba de la mierda: idealmente, aplique un enfoque TDD en el que escriba una prueba para el código que va a realizar en multiproceso para asegurarse de que da el resultado correcto después de que termine … aunque las condiciones de carrera son el tipo de cosas que Vuele fácilmente bajo el radar de la unidad y las pruebas de integración, por lo que, una vez más, es absolutamente necesario que pueda comprender la totalidad de los efectos secundarios que se producen en una determinada pieza de código antes de intentar multiproceso. 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 solo un tipo de efecto secundario para un bucle completo.

Es difícil darle pautas adecuadas. Pero…

La forma más fácil de salir de mi parte es convertir su aplicación a ActiveX EXE, ya que COM tiene soporte para subprocesos, etc., integrado en su progtwig, se convertirá automáticamente en una aplicación de subprocesos múltiples. Por supuesto, tendrá que hacer algunos cambios en su código. Pero esta es la forma más corta y segura de ir.

No estoy seguro, pero es probable que la biblioteca RichClient Toolset haga el truco por ti. En el sitio el autor ha escrito:

También ofrece capacidades de carga / creación de instancias sin registro para ActiveX-Dlls y un enfoque de subprocesos nuevo y fácil de usar, que funciona con Named-Pipes debajo del capó y, por lo tanto, también funciona en procesos cruzados.

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 continuar utilizando lo que se proporciona en su IDE de elección integrándolo con SVN a través de complementos.

Olvidé mencionar que hemos completado una aplicación para el mercado de acciones que se intercambia automáticamente (compra y venta basada en mínimos y máximos) en los scripts que están en la cartera de usuarios basados ​​en un algoritmo que hemos desarrollado.

Al desarrollar este software, nos enfrentamos al mismo tipo de problema que usted ha ilustrado aquí. Para resolverlo, convertimos la aplicación en ActiveX EXE y convertimos todas aquellas partes que necesitan ejecutarse paralelamente en DLL ActiveX. ¡No hemos utilizado ninguna biblioteca de terceros para esto!

HTH