usar thread library how ejemplos ejemplo algorithms c++ multithreading

how - thread library c++



¿Cuáles son las "cosas que debe saber" cuando se sumerge en la programación de subprocesos múltiples en C++? (22)

Además de las otras cosas mencionadas, debe aprender sobre las colas de mensajes asincrónicos. Pueden resolver de forma elegante los problemas del intercambio de datos y el manejo de eventos. Este enfoque funciona bien cuando tiene máquinas de estado simultáneas que necesitan comunicarse entre sí.

No estoy al tanto de ningún marco de paso de mensajes diseñado para funcionar solo en el nivel de subprocesos. Solo he visto soluciones caseras. Por favor comente si sabe de alguno existente.

EDITAR:

Uno podría usar las colas sin bloqueo TBB de TBB , ya sea tal como está o como base para una cola de paso de mensajes más general.

Actualmente estoy trabajando en una aplicación de red inalámbrica en C ++ y está llegando a un punto en el que voy a querer trocear piezas de software en un solo proceso, en lugar de tenerlas todas en procesos separados. Teóricamente, entiendo multi-threading, pero todavía tengo que bucear prácticamente.

¿Qué debería saber todo programador al escribir código multiproceso en C ++?


Aléjate de MFC y es una biblioteca de mensajes multiproceso +.
De hecho, si ve MFC y los hilos que vienen hacia usted, corra hacia las colinas (*)

(*) A menos que, por supuesto, si MFC viene de las colinas, en cuyo caso corres LEJOS de las colinas.


Algunas de las respuestas han tocado esto, pero quería enfatizar un punto: si puede, asegúrese de que solo se pueda acceder a la mayor cantidad posible de sus datos de un hilo a la vez . Las colas de mensajes son una construcción muy útil para usar para esto.

No he tenido que escribir mucho código con muchos subprocesos en C ++, pero en general, el patrón productor-consumidor puede ser muy útil para utilizar múltiples subprocesos de manera eficiente, al tiempo que se evitan las condiciones de carrera asociadas con el acceso concurrente.

Si puedes usar el código ya depurado de otra persona para manejar la interacción de subprocesos, estás en buena forma. Como principiante, existe la tentación de hacer las cosas de forma ad hoc, por ejemplo, para usar una variable "volátil" para sincronizar dos códigos. Evita eso tanto como sea posible. Es muy difícil escribir código que sea a prueba de balas en presencia de subprocesos contendientes, así que encuentre un código en el que pueda confiar y minimice el uso de las primitivas de bajo nivel tanto como sea posible.


Antes de dar cualquier consejo sobre qué hacer y qué no hacer sobre la programación de múltiples hilos en C ++, me gustaría formular la pregunta: ¿Hay algún motivo particular por el que quiera comenzar a escribir la aplicación en C ++?

Existen otros paradigmas de programación en los que se utilizan los múltiples núcleos sin entrar en la programación de subprocesos múltiples. Uno de esos paradigmas es la programación funcional. Escribe cada parte de tu código como funciones sin ningún efecto secundario. Entonces es fácil ejecutarlo en múltiples hilos sin preocuparse por la sincronización.

Estoy usando Erlang para mi propósito de desarrollo. Ha aumentado en productividad en al menos un 50%. El código que se ejecuta puede no ser tan rápido como el código escrito en C ++. Pero he notado que para la mayoría del procesamiento de datos fuera de línea de back-end, la velocidad no es tan importante como la distribución del trabajo y la utilización del hardware tanto como sea posible. Erlang proporciona un modelo de simultaneidad simple donde puede ejecutar una sola función en múltiples hilos sin preocuparse por el problema de sincronización. Escribir código multiproceso es fácil, pero la depuración lleva mucho tiempo. He realizado programación de subprocesos múltiples en C ++, pero actualmente estoy satisfecho con el modelo de simultaneidad de Erlang. Vale la pena investigar.


Asegúrese de probar su código en un sistema de una sola CPU y en un sistema multi-cpu.

En base a los comentarios:

  • Toma única, de un solo núcleo
  • Socket único, dos núcleos
  • Toma única, más de dos núcleos
  • Dos enchufes, de un solo núcleo cada uno
  • Dos enchufes, combinación de CPUs individuales, duales y multinúcleo
  • Enchufes múltiples, combinación de CPU individuales, duales y multinúcleo

El factor limitante aquí va a ser el costo. Idealmente, concéntrese en los tipos de sistema en los que se ejecutará su código.


Asegúrese de saber explícitamente qué objetos se comparten y cómo se comparten.

Tanto como sea posible, haga que sus funciones sean puramente funcionales. Es decir, tienen entradas y salidas y no tienen efectos secundarios. Esto hace que sea mucho más simple razonar acerca de su código. Con un programa más simple no es tan importante, pero a medida que la complejidad aumenta se volverá esencial. Los efectos secundarios son los que conducen a problemas de seguridad de subprocesos.

Juega al abogado del diablo con tu código. Mire un código y piense cómo podría romper esto con un entrelazado de hilos bien sincronizado. En algún momento este caso sucederá.

Primero aprende sobre seguridad de hilos. Una vez que entiendes eso, pasas a la parte difícil: el rendimiento concurrente. Aquí es donde es esencial alejarse de los bloqueos globales. Averiguar formas de minimizar y eliminar bloqueos mientras se mantiene la seguridad del hilo es difícil.


Asegúrese de saber qué significa volatile y sus usos (lo que puede no ser obvio al principio).

Además, al diseñar código multiproceso, es útil imaginar que una cantidad infinita de procesadores está ejecutando cada línea de código en su aplicación a la vez. (es decir, cada línea de código posible de acuerdo con su lógica en su código.) Y que todo lo que no esté marcado como volátil el compilador realiza una optimización especial para que solo el hilo que lo modificó pueda leer / establecer su true value and all the other threads get garbage.


Como eres un principiante, comienza de manera simple. Primero haz que funcione correctamente, luego preocúpate por las optimizaciones. He visto a personas tratar de optimizar aumentando la concurrencia de una sección de código en particular (a menudo usando trucos dudosos), sin siquiera mirar para ver si hubo alguna contención en primer lugar.

En segundo lugar, desea poder trabajar a un nivel tan alto como sea posible. No trabaje al nivel de bloqueos y mutex si puede usar una cola de maestro-trabajador existente. El TBB de Intel parece prometedor, ya que tiene un nivel ligeramente superior al de los hilos puros.

En tercer lugar, la programación de subprocesos múltiples es difícil. Reduzca las áreas de su código donde tenga que pensar tanto como sea posible. Si puede escribir una clase de modo que los objetos de esa clase solo se operen en un solo hilo, y no haya datos estáticos, esto reduce en gran medida las cosas de las que debe preocuparse en la clase.


Debe tener una comprensión de la programación básica de sistemas, en particular:

  • E / S síncrona frente a asíncrona (bloqueo frente a no bloqueo)
  • Mecanismos de sincronización, como construcciones de bloqueo y mutex
  • Gestión de subprocesos en su plataforma de destino

Debes leer sobre bloqueos, mutexes, semáforos y variables de condición.

Un consejo: si su aplicación tiene alguna forma de UI, asegúrese de cambiarla siempre de la secuencia de la interfaz de usuario. La mayoría de los toolkits / frameworks de UI se bloquean (o se comportan de forma inesperada) si se accede desde un hilo de fondo. Por lo general, proporcionan algún tipo de método de envío para ejecutar alguna función en el hilo de la interfaz de usuario.


Estoy en el mismo barco que tú, recién estoy comenzando a hacer subprocesos múltiples por primera vez como parte de un proyecto y he estado buscando recursos en la red. Encontré que this blog es muy informativo. La parte 1 es pthreads, pero he vinculado el inicio en la sección de impulso.


Estoy exactamente en esta situación: escribí una biblioteca con un bloqueo global (muchos hilos, pero solo uno que se ejecuta a la vez en la biblioteca) y estoy refabricando para admitir la concurrencia.

He leído libros sobre el tema, pero lo que aprendí se destaca en algunos puntos:

  1. pensar en paralelo : imaginar una multitud pasando por el código. ¿Qué sucede cuando se llama un método mientras ya está en acción?
  2. pensar compartido : imaginar muchas personas tratando de leer y alterar recursos compartidos al mismo tiempo.
  3. diseño : evite los problemas que pueden generar los puntos 1 y 2.
  4. nunca pienses que puedes ignorar los casos extremos, te morderán duro.

Como no puede probar un diseño simultáneo (porque el entrelazado de ejecución de subprocesos no es reproducible), debe asegurarse de que su diseño sea robusto analizando cuidadosamente las rutas de código y documentando cómo se supone que se utilizará el código.

Una vez que comprenda cómo y dónde debe embotellar su código, puede leer la documentación sobre las herramientas utilizadas para este trabajo:

  1. Mutex (acceso exclusivo a un recurso)
  2. Bloqueos de alcance (buen patrón para bloquear / desbloquear un Mutex)
  3. Semáforos (pasando información entre hilos)
  4. ReadWrite Mutex (muchos lectores, acceso exclusivo en escritura)
  5. Señales (cómo "matar" un hilo o enviar una señal de interrupción, cómo atraparlos)
  6. Patrones de diseño en paralelo: jefe / trabajador, productor / consumidor, etc. (ver schmidt )
  7. herramientas específicas de la plataforma: openMP, bloques C, etc.

Buena suerte ! La concurrencia es divertida, solo tómate tu tiempo ...


He escrito una aplicación de servidor multiproceso y un shellsort multiproceso. Ambos fueron escritos en C y usan las funciones de enhebrado de NT "en bruto" que están sin ninguna biblioteca de funciones intermedia para confundir las cosas. Eran dos experiencias bastante diferentes con diferentes conclusiones que extraer. El alto rendimiento y la alta confiabilidad fueron las principales prioridades, aunque las prácticas de codificación tenían una mayor prioridad si uno de los dos primeros se consideraba amenazado a largo plazo.

La aplicación del servidor tenía tanto un servidor como una parte del cliente y utilizaba iocps para administrar solicitudes y respuestas. Cuando utilice iocps, es importante que nunca use más hilos que núcleos. También encontré que las solicitudes a la parte del servidor necesitaban una mayor prioridad para no perder ninguna solicitud innecesariamente. Una vez que estuvieran "seguros", podría usar subprocesos de menor prioridad para crear las respuestas del servidor. Juzgué que la parte del cliente podría tener una prioridad aún menor. Hice las preguntas "¿qué datos no puedo perder?" y "¿qué datos puedo permitir que fallen porque siempre puedo volver a intentarlo?" También necesitaba poder interactuar con la configuración de la aplicación a través de una ventana y tenía que ser receptivo. El truco fue que la interfaz de usuario tenía prioridad normal, las solicitudes entrantes una menos y así sucesivamente. Mi razonamiento detrás de esto es que, dado que usaré la IU tan pocas veces, puede tener la más alta prioridad para que cuando lo use responda de inmediato. Enhebrar aquí resultó significar que todas las partes separadas del programa en el caso normal podrían / ​​podrían estar funcionando simultáneamente, pero cuando el sistema estaba bajo mayor carga, la potencia de procesamiento se desplazaría a las partes vitales debido al esquema de priorización.

Siempre me ha gustado el shellsort, así que por favor, evita los punteros sobre quicksort esto o aquello o blablabla. O sobre cómo shellsort no es adecuado para multihilo. Habiendo dicho eso, el problema que tuve que hacer fue ordenar una lista de unidades semi-large en la memoria (para mis pruebas utilicé una lista ordenada inversamente de un millón de unidades de cuarenta bytes cada una. Utilizando un shellsort de un solo subproceso pude ordenar ellos a una velocidad de aproximadamente una unidad cada dos años (microsegundos). Mi primer intento de multiprocesamiento fue con dos hilos (aunque pronto me di cuenta de que quería poder especificar el número de hilos) y se ejecutó en aproximadamente una unidad cada 3.5 segundos, es decir, más LENTO. El uso de un generador de perfiles ayudó mucho y un cuello de botella resultó ser el registro estadístico (es decir, compara e intercambia) donde los hilos chocarían entre ellos. Dividir los datos entre los hilos de una manera eficiente resultó ser el mayor desafío y definitivamente hay más cosas que puedo hacer, como dividir el vector que contiene los indeces en unidades adaptadas al tamaño de la línea de caché y quizás también comparar todos los indeces en dos líneas de caché antes de pasar a la siguiente línea (al menos yo Creo que hay algo que puedo hacer allí: los algoritmos se vuelven bastante complicados). Al final, logré una tasa de una unidad cada microsegundo con tres hilos simultáneos (cuatro hilos aproximadamente iguales, solo tenía cuatro núcleos disponibles).

En cuanto a la pregunta original, mi consejo para usted sería

  1. Si tiene tiempo, aprenda el mecanismo de enhebrado al nivel más bajo posible.
  2. Si el rendimiento es importante, conozca los mecanismos relacionados que proporciona el sistema operativo. Multi-threading por sí mismo rara vez es suficiente para lograr el máximo potencial de una aplicación.
  3. Use perfiles para comprender las peculiaridades de múltiples hilos que trabajan en la misma memoria.
  4. El trabajo arquitectónico descuidado matará a cualquier aplicación, independientemente de cuántos núcleos y sistemas tenga y sin importar la brillantez de sus programadores.
  5. La programación descuidada matará cualquier aplicación, independientemente de la brillantez de la base arquitectónica.
  6. Comprenda que el uso de bibliotecas le permite alcanzar el objetivo de desarrollo más rápidamente, pero a costa de una menor comprensión y (por lo general) menor rendimiento.

La mayor diferencia de "mentalidad" entre la programación de subproceso único y la de subproceso múltiple en mi opinión es en pruebas / verificación. En la programación de un único subproceso, las personas suelen descifrar un código medio pensado, ejecutarlo y, si parece funcionar, lo llamarán bueno y, a menudo, se saldrá con la suya en un entorno de producción.

En la programación multiproceso, por otro lado, el comportamiento del programa no es determinista, porque la combinación exacta del momento en que se están ejecutando los subprocesos para qué períodos de tiempo (relativos entre sí) será diferente cada vez que se ejecuta el programa. Así que simplemente ejecutando un programa multiproceso varias veces (o incluso unos pocos millones de veces) y diciendo "no se estrelló para mí, ¡entrégalo!" es completamente inadecuado.

En cambio, cuando se hace un programa multiproceso, siempre se debe tratar de probar (al menos para su propia satisfacción) que no solo funciona el programa, sino que no hay forma de que no funcione . Esto es mucho más difícil, porque en lugar de verificar una única ruta de código, está intentando verificar una cantidad casi infinita de posibles rutas de código.

La única forma realista de hacerlo sin que su cerebro explote es mantener las cosas tan sencillas como sea posible. Si puedes evitar usar multihilos totalmente, haz eso. Si debe hacer multiprocesamiento, comparta la menor cantidad posible de datos entre subprocesos y utilice las primitivas de subprocesamiento múltiple adecuadas (por ejemplo, mutexes, colas de mensajes seguros para subprocesos, condiciones de espera) y no intente salirse con medias tintas (por ejemplo, tratando de sincronizar el acceso a una pieza de datos compartida usando solo indicadores booleanos nunca funcionará de manera confiable, así que no lo intentes)

Lo que quiere evitar es el escenario infernal de subprocesos múltiples: el programa multiproceso que se ejecuta felizmente durante semanas en su máquina de prueba, pero se bloquea aleatoriamente, aproximadamente una vez al año, en el sitio del cliente. Ese tipo de error de condición de carrera puede ser casi imposible de reproducir, y la única forma de evitarlo es diseñar su código con extremo cuidado para garantizar que no pueda suceder.

Los hilos son fuertes juju. Úselos con moderación.


Mantenga todo lo muerto simple tanto como sea posible. Es mejor tener un diseño más simple (mantenimiento, menos errores) que una solución más compleja que podría tener una utilización de la CPU ligeramente mejor.

Evite compartir estado entre subprocesos tanto como sea posible, esto reduce el número de lugares que deben usar la sincronización.

Evite compartir falsamente a toda costa (google this term).

Utilice un grupo de subprocesos para no crear / destruir subprocesos con frecuencia (eso es costoso y lento).

Considere usar OpenMP, Intel y Microsoft (posiblemente otros) apoyan esta extensión a C ++.

Si está haciendo números crujientes, considere usar Intel IPP, que internamente utiliza funciones optimizadas SIMD (esto no es realmente multi-threading, pero es paralelismo de un tipo relacionado).

Diviértete muchísimo.


Me centraría en diseñar la cosa tanto como particiones como sea posible para que tenga la cantidad mínima de elementos compartidos en los hilos. Si se asegura de que no tenga statics ni otros recursos compartidos entre hilos (distintos de los que compartiría si diseñara esto con procesos en lugar de hilos), estaría bien.

Por lo tanto, mientras que sí, debes tener en cuenta conceptos como bloqueos, semáforos, etc., la mejor manera de abordar esto es tratar de evitarlos.


Me pareció útil ver las conferencias introductorias sobre programación de sistema operativo y sistemas por John Kubiatowicz en Berkeley.


Mis mejores consejos para enredar a los novatos:

  • Si es posible, use una biblioteca de paralelismo basada en tareas , siendo la TBB de Intel la más obvia. Esto lo aísla de los detalles sucios y complicados, y es más eficiente que cualquier cosa que improvise usted mismo. La desventaja principal es que este modelo no admite todos los usos de multihilo; es genial para explotar multinúcleos para la potencia de cómputo, menos útil si quieres que los subprocesos esperen para bloquear la E / S.

  • Sepa cómo abortar los hilos (o en el caso de TBB, cómo hacer que las tareas se completen temprano cuando usted decide que no quiere los resultados después de todo). Los principiantes parecen ser atraídos por las funciones de matar hilos como polillas a una llama. No lo hagas ... Herb Sutter tiene un excelente artículo corto sobre esto.


No soy un experto en absoluto en este tema. Solo algunas reglas generales:

1) Diseñe para simplificar , los errores realmente son difíciles de encontrar en código concurrente incluso en los ejemplos más simples.
2) C ++ le ofrece un paradigma muy elegante para gestionar recursos (mutex, semáforo, ...): RAII . Observé que es mucho más fácil trabajar con boost::thread que trabajar con hilos POSIX .
3) Construye tu código como hilo seguro . Si no lo hace, su programa podría comportarse de manera extraña .


Nunca suponga que las API externas son seguras para hilos. Si no está explícitamente establecido en sus documentos, no los llame al mismo tiempo desde múltiples hilos. En cambio, limite su uso de ellos a un solo hilo o use un mutex para evitar llamadas simultáneas (esto es bastante similar a las bibliotecas de GUI antes mencionadas).

El siguiente punto está relacionado con el lenguaje. Recuerde, C ++ tiene (actualmente) un enfoque bien definido para enhebrar. El compilador / optimizador no sabe si el código puede ser llamado concurrentemente. La palabra clave volatile es útil para evitar ciertas optimizaciones (es decir, el almacenamiento en memoria caché de campos de memoria en los registros de la CPU) en contextos de subprocesos múltiples, pero no es un mecanismo de sincronización.

Recomiendo impulsar para primitivas de sincronización. No te metas con las API de la plataforma. Hacen que su código sea difícil de transportar porque tienen una funcionalidad similar en todas las plataformas principales, pero un comportamiento de detalle ligeramente diferente. Boost resuelve estos problemas al exponer al usuario solo funcionalidades comunes.

Además, si existe la menor posibilidad de que una estructura de datos pueda ser escrita por dos hilos al mismo tiempo, utilice una primitiva de sincronización para protegerla. Incluso si crees que solo sucederá una vez en un millón de años.


Parte de mi área de estudio de posgrado se relaciona con el paralelismo.

Leí este book y encontré que es un buen resumen de los enfoques en el nivel de diseño.

En el nivel técnico básico, tienes 2 opciones básicas: hilos o mensajes que pasan. Las aplicaciones con subprocesos son las más fáciles de despegar, ya que subprocesos, subprocesos de Windows o subprocesos están listos para funcionar. Sin embargo, trae consigo la complejidad de la memoria compartida.

La usabilidad de paso de mensajes parece en general limitada en este punto a la API de MPI. Configura un entorno donde puede ejecutar trabajos y particionar su programa entre procesadores. Es más para entornos de supercomputadora / clúster donde no hay memoria compartida intrínseca. Puede lograr resultados similares con sockets y demás.

En otro nivel, puede usar pragmas de tipo de lenguaje: el popular de hoy es OpenMP. No lo he usado, pero parece construir subprocesos a través de preprocesamiento o una biblioteca de tiempo de enlace.

El problema clásico es la sincronización aquí; todos los problemas en la multiprogramación provienen de la naturaleza no determinista de los multiprogramas, que no pueden evitarse.

Vea los métodos de sincronización de Lamport para una discusión adicional de sincronizaciones y tiempos.

El multihilo no es algo que solo los doctores y gurús puedan hacer, pero tendrás que ser bastante bueno para hacerlo sin cometer errores demenciales.


Una cosa que he encontrado muy útil es hacer que la aplicación se pueda configurar con respecto a la cantidad real de hilos que usa para varias tareas. Por ejemplo, si tiene múltiples hilos que acceden a una base de datos, haga que el número de esos hilos sea configurable a través de un parámetro de línea de comando. Esto es extremadamente útil cuando se depura: puede excluir problemas de enhebrado estableciendo el número en 1, o forzarlos al configurarlo en un número alto. También es muy útil para determinar cuál es la cantidad óptima de hilos.