¿Por qué a menudo se prefiere el subprocesamiento múltiple para mejorar el rendimiento?


23

Tengo una pregunta, se trata de por qué los programadores parecen amar la concurrencia y los programas multiproceso en general.

Estoy considerando 2 enfoques principales aquí:

  • un enfoque asíncrono básicamente basado en señales, o simplemente un enfoque asíncrono como lo llaman muchos documentos e idiomas como el nuevo C # 5.0, por ejemplo, y un "hilo complementario" que administra la política de su canalización
  • un enfoque concurrente o enfoque de subprocesos múltiples

Solo diré que estoy pensando en el hardware aquí y en el peor de los casos, y he probado estos 2 paradigmas, el paradigma asíncrono es un ganador en el punto en que no entiendo por qué las personas el 90% del tiempo Hablen de subprocesos múltiples cuando quieran acelerar las cosas o aprovechar sus recursos.

He probado programas multiproceso y un programa asíncrono en una máquina antigua con un procesador Intel de cuatro núcleos que no ofrece un controlador de memoria dentro de la CPU, la memoria es administrada completamente por la placa base, bueno, en este caso, el rendimiento es horrible con un aplicación multiproceso, incluso un número relativamente bajo de subprocesos como 3-4-5 puede ser un problema, la aplicación no responde y es lenta y desagradable.

Un buen enfoque asíncrono es, por otro lado, probablemente no más rápido, pero tampoco es peor, mi aplicación solo espera el resultado y no se cuelga, responde y hay una escala mucho mejor.

También descubrí que un cambio de contexto en el mundo de los subprocesos no es tan barato en el escenario del mundo real, de hecho es bastante costoso, especialmente cuando tiene más de 2 subprocesos que necesitan alternar e intercambiarse entre sí para ser computados.

En las CPU modernas, la situación no es realmente tan diferente, el controlador de memoria está integrado, pero mi punto es que una CPU x86 es básicamente una máquina en serie y el controlador de memoria funciona de la misma manera que con la máquina anterior con un controlador de memoria externo en la placa base . El cambio de contexto sigue siendo un costo relevante en mi aplicación y el hecho de que el controlador de memoria esté integrado o que la CPU más nueva tenga más de 2 núcleos no es una ganga para mí.

Por lo que he experimentado, el enfoque concurrente es bueno en teoría pero no tan bueno en la práctica, con el modelo de memoria impuesto por el hardware, es difícil hacer un buen uso de este paradigma, también presenta muchos problemas que van desde el uso de mis estructuras de datos a la unión de múltiples hilos.

Además, ambos paradigmas no ofrecen ningún tipo de seguridad cuando la tarea o el trabajo se realizarán en un determinado momento, lo que los hace realmente similares desde un punto de vista funcional.

Según el modelo de memoria X86, ¿por qué la mayoría de las personas sugiere utilizar la concurrencia con C ++ y no solo un enfoque asíncrono? Además, ¿por qué no considerar el peor de los casos de una computadora donde el cambio de contexto es probablemente más costoso que la computación misma?


2
Una forma de comparar sería mirar el mundo de JavaScript, donde no hay subprocesos y todo es agresivamente asíncrono, utilizando devoluciones de llamada. Funciona, pero tiene sus propios problemas.
Gort the Robot

2
@StevenBurnap ¿Cómo se llama a los trabajadores web?
user16764

2
"Incluso un número relativamente bajo de subprocesos como 3-4-5 puede ser un problema, la aplicación no responde y es lenta y desagradable". => Podría deberse a un diseño deficiente / uso inapropiado de hilos. Por lo general, se encuentra ese tipo de situación cuando sus hilos siguen intercambiando datos, en cuyo caso el multihilo puede no ser la respuesta correcta o puede que necesite volver a particionar los datos.
assylias

1
@assylias Ver una desaceleración significativa en el subproceso de la interfaz de usuario indica una cantidad excesiva de bloqueo entre subprocesos. O tiene una implementación deficiente o está tratando de golpear una clavija cuadrada en un agujero redondo.
Evan Plaice

55
Dices que "los programadores parecen amar la concurrencia y los programas de subprocesos múltiples en general" Lo dudo. Yo diría que "los programadores lo odian" ... pero a menudo es lo único que puede hacer ...
johannes

Respuestas:


34

Tiene varios núcleos / procesors, utilizar las

Async es mejor para hacer un procesamiento pesado con enlace de E / S, pero ¿qué pasa con el procesamiento pesado con enlace de CPU?

El problema surge cuando los bloques de código de subproceso único (es decir, se atascan) en un proceso de larga ejecución. Por ejemplo, ¿recuerda cuando la impresión de un documento con procesador de textos haría que toda la aplicación se congelara hasta que se enviara el trabajo? La congelación de aplicaciones es un efecto secundario de un bloqueo de aplicaciones de un solo subproceso durante una tarea intensiva de CPU.

En una aplicación multiproceso, las tareas intensivas de CPU (por ejemplo, un trabajo de impresión) se pueden enviar a un subproceso de trabajo en segundo plano, liberando así el subproceso de la interfaz de usuario.

Del mismo modo, en una aplicación multiproceso, el trabajo se puede enviar a través de mensajes (por ejemplo, IPC, sockets, etc.) a un subproceso diseñado específicamente para procesar trabajos.

En la práctica, el código asíncrono y multiproceso / proceso tiene sus ventajas y desventajas.

Puede ver la tendencia en las principales plataformas en la nube, ya que ofrecerán instancias especializadas para el procesamiento vinculado a la CPU e instancias especializadas para el procesamiento vinculado a E / S.

Ejemplos:

  • El almacenamiento (ex Amazon S3, Google Cloud Drive) está vinculado a la CPU
  • Los servidores web están vinculados a IO (Amazon EC2, Google App Engine)
  • Las bases de datos son ambas, la CPU está vinculada a las escrituras / indexación y la IO a las lecturas

Para ponerlo en perspectiva ...

Un servidor web es un ejemplo perfecto de una plataforma que está fuertemente vinculada a IO. Un servidor web multiproceso que asigna un subproceso por conexión no se escala bien porque cada subproceso incurre en más sobrecarga debido a la mayor cantidad de cambio de contexto y bloqueo de subprocesos en los recursos compartidos. Mientras que un servidor web asíncrono usaría un solo espacio de direcciones.

Del mismo modo, una aplicación especializada para codificar video funcionaría mucho mejor en un entorno de subprocesos múltiples porque el procesamiento pesado involucrado bloquearía el hilo principal hasta que se realizara el trabajo. Hay maneras de mitigar esto, pero es mucho más fácil tener un solo subproceso que administre una cola, un segundo subproceso que administra la limpieza y un grupo de subprocesos que administran el procesamiento pesado. La comunicación entre subprocesos ocurre solo cuando las tareas se asignan / completan, por lo que la sobrecarga de bloqueo de subprocesos se mantiene al mínimo.

La mejor aplicación a menudo usa una combinación de ambos. Una aplicación web, por ejemplo, puede usar nginx (es decir, un único subproceso asíncrono) como equilibrador de carga para administrar el torrente de solicitudes entrantes, un servidor web asíncrono similar (ex Node.js) para manejar solicitudes http y un conjunto de servidores multiproceso manejar la carga / transmisión / codificación de contenido, etc.

A lo largo de los años, ha habido muchas guerras religiosas entre modelos multiproceso, multiproceso y asíncronos. Como con la mayoría de las cosas, la mejor respuesta debería ser "depende".

Sigue la misma línea de pensamiento que justifica el uso de arquitecturas de GPU y CPU en paralelo. Dos sistemas especializados que se ejecutan en concierto pueden tener una mejora mucho mayor que un solo enfoque monolítico.

Ninguno de los dos es mejor porque ambos tienen sus usos. Use la mejor herramienta para el trabajo.

Actualizar:

Eliminé la referencia a Apache e hice una pequeña corrección. Apache utiliza un modelo multiproceso que bifurca un proceso para cada solicitud, lo que aumenta la cantidad de cambio de contexto a nivel del núcleo. Además, dado que la memoria no se puede compartir entre procesos, cada solicitud conlleva un costo de memoria adicional.

Los subprocesos múltiples evitan la necesidad de memoria adicional porque se basa en una memoria compartida entre subprocesos. La memoria compartida elimina la sobrecarga de memoria adicional, pero aún incurre en la penalidad de un mayor cambio de contexto. Además, para garantizar que no se den las condiciones de carrera, se requieren bloqueos de subprocesos (que garantizan el acceso exclusivo a un solo subproceso a la vez) para los recursos que se comparten entre subprocesos.

Es curioso que digas, "los programadores parecen amar la concurrencia y los programas de subprocesos múltiples en general". La programación multiproceso es temida universalmente por cualquiera que haya realizado una cantidad sustancial en su tiempo. Los bloqueos muertos (un error que ocurre cuando un recurso es bloqueado por error por dos fuentes diferentes que impiden que se termine) y las condiciones de carrera (donde el programa generará el resultado incorrecto al azar por error debido a una secuencia incorrecta) son algunas de las más difíciles de rastrear abajo y arreglar.

Actualización2:

Contrariamente a la declaración general acerca de que IPC es más rápido que las comunicaciones de red (es decir, socket). Ese no es siempre el caso . Tenga en cuenta que estas son generalizaciones y los detalles específicos de la implementación pueden tener un gran impacto en el resultado.


¿Por qué un programador debe ir multiproceso? Quiero decir, supongo que con más de 1 proceso también necesita algún tipo de comunicación entre procesos que pueda agregar una sobrecarga significativa, ¿es algo así como la antigua forma de hacer las cosas del programador de Windows? ¿Cuándo debo ir multiproceso? Gracias por su respuesta, por cierto, una muy buena imagen de para qué son asíncronos y multiproceso.
user1849534

1
Está suponiendo que la comunicación entre procesos aumentaría la sobrecarga general. Sin embargo, si el estado de procesamiento es inmutable, o solo necesita manejar la sincronización al inicio / finalización. puede ser mucho más eficiente desplegarse en tareas más paralelas. El patrón de actor es un buen ejemplo, y si no lo has leído, realmente vale la pena seguir leyendo. akka.io
sylvanaar

1
@ user1849534 Varios hilos pueden comunicarse entre sí a través de memoria compartida + bloqueo o IPC. El bloqueo es más fácil pero más difícil de depurar si comete un error (por ejemplo, perdió un bloqueo, bloqueo muerto). IPC es mejor si tiene muchos subprocesos de trabajo porque el bloqueo no escala bien. De cualquier manera, si está utilizando un enfoque de subprocesos múltiples, es importante mantener la comunicación / sincronización entre subprocesos al mínimo absoluto (es decir, para minimizar la sobrecarga).
Evan Plaice

1
@ akka.io Tienes toda la razón. La inmutabilidad es una forma de minimizar / eliminar la sobrecarga del bloqueo, pero aún así se incurre en el costo de tiempo del cambio de contexto. Si desea ampliar la respuesta para incluir los detalles sobre cómo la inmutabilidad puede resolver los problemas de sincronización de subprocesos, no dude en hacerlo. El punto principal que pretendía ilustrar es que hay casos en los que la comunicación asincrónica tiene una clara ventaja sobre los procesos / subprocesos múltiples y viceversa.
Evan Plaice

(cont.) Pero, sinceramente, si necesitaba mucha capacidad de procesamiento dependiente de la CPU, omitiría el modelo de actor y lo desarrollaría para poder escalar a múltiples nodos de red. La mejor solución que he visto para esto es usar el modelo de ventilador de tareas de 0MQ sobre comunicaciones a nivel de socket. Ver Fig. 5 @ zguide.zeromq.org/page:all .
Evan Plaice

13

El enfoque asincrónico de Microsoft es un buen sustituto para los propósitos más comunes de la programación multiproceso: mejorar la capacidad de respuesta con respecto a las tareas de E / S.

Sin embargo, es importante darse cuenta de que el enfoque asincrónico no es capaz de mejorar el rendimiento o mejorar la capacidad de respuesta con respecto a las tareas intensivas de la CPU.

Multithreading para receptividad

El subprocesamiento múltiple para la capacidad de respuesta es la forma tradicional de mantener un programa receptivo durante tareas de E / S pesadas o tareas de computación pesadas. Guarda los archivos en un subproceso en segundo plano, para que el usuario pueda continuar su trabajo, sin tener que esperar a que el disco duro termine su tarea. El hilo IO a menudo bloquea la espera de que termine una parte de una escritura, por lo que los cambios de contexto son frecuentes.

De manera similar, al realizar un cálculo complejo, desea permitir el cambio de contexto regular para que la interfaz de usuario pueda seguir respondiendo y el usuario no piense que el programa se ha bloqueado.

El objetivo aquí no es, en general, hacer que los múltiples subprocesos se ejecuten en diferentes CPU. En cambio, solo estamos interesados ​​en lograr que se produzcan cambios de contexto entre la tarea en segundo plano de larga ejecución y la IU, de modo que la IU pueda actualizarse y responder al usuario mientras se ejecuta la tarea en segundo plano. En general, la interfaz de usuario no ocupará mucha energía de la CPU, y el marco de trabajo o el sistema operativo generalmente decidirán ejecutarlos en la misma CPU.

De hecho, perdemos el rendimiento general debido al costo adicional del cambio de contexto, pero no nos importa porque el rendimiento de la CPU no era nuestro objetivo. Sabemos que generalmente tenemos más potencia de CPU de la que necesitamos, por lo que nuestro objetivo con respecto al subprocesamiento múltiple es realizar una tarea para el usuario sin perder el tiempo del usuario.

La alternativa "asincrónica"

El "enfoque asincrónico" cambia esta imagen al habilitar cambios de contexto dentro de un solo hilo. Esto garantiza que todas nuestras tareas se ejecutarán en una sola CPU y puede proporcionar algunas mejoras de rendimiento modestas en términos de menos creación / limpieza de subprocesos y menos cambios de contexto real entre subprocesos.

En lugar de crear un nuevo hilo para esperar la recepción de un recurso de red (por ejemplo, descargar una imagen), asyncse utiliza un método, que awaites la imagen que está disponible y, mientras tanto, cede al método de llamada.

La principal ventaja aquí es que no tiene que preocuparse por problemas de enhebrado, como evitar el punto muerto, ya que no está usando bloqueos y sincronización, y hay un poco menos de trabajo para el programador que configura el hilo de fondo y regresa en el subproceso de la interfaz de usuario cuando vuelve el resultado para actualizar la interfaz de usuario de forma segura.

No he profundizado demasiado en los detalles técnicos, pero mi impresión es que administrar la descarga con actividad ocasional de la CPU se convierte en una tarea no para un hilo separado, sino más bien como una tarea en la cola de eventos de la interfaz de usuario, y cuando la descarga se completa, el método asincrónico se reanuda desde esa cola de eventos. En otras palabras, awaitsignifica algo parecido a "verificar si el resultado que necesito está disponible, si no, volver a ponerme en la cola de tareas de este hilo".

Tenga en cuenta que este enfoque no resolvería el problema de una tarea intensiva de CPU: no hay datos que esperar, por lo que no podemos obtener los cambios de contexto que necesitamos que sucedan sin crear un subproceso de trabajo de fondo real. Por supuesto, aún podría ser conveniente utilizar un método asincrónico para iniciar el subproceso en segundo plano y devolver el resultado, en un programa que utiliza de forma generalizada el enfoque asincrónico.

Multithreading para rendimiento

Dado que habla sobre el "rendimiento", también me gustaría analizar cómo se puede utilizar el subprocesamiento múltiple para obtener ganancias de rendimiento, algo que es completamente imposible con el enfoque asincrónico de subproceso único.

Cuando en realidad se encuentra en una situación en la que no tiene suficiente potencia de CPU en una sola CPU y desea utilizar subprocesos múltiples para el rendimiento, a menudo es difícil hacerlo. Por otro lado, si una CPU no tiene suficiente potencia de procesamiento, a menudo también es la única solución que podría permitir que su programa haga lo que le gustaría lograr en un plazo razonable, que es lo que hace que el trabajo valga la pena.

Paralelismo Trivial

Por supuesto, a veces puede ser fácil obtener una aceleración real de subprocesos múltiples.

Si tiene una gran cantidad de tareas independientes intensivas en cómputo (es decir, tareas cuyos datos de entrada y salida son muy pequeños con respecto a los cálculos que deben realizarse para determinar el resultado), a menudo puede obtener una aceleración significativa al crear un grupo de subprocesos (de tamaño apropiado según el número de CPU disponibles) y hacer que un subproceso maestro distribuya el trabajo y recopile los resultados.

Multithreading práctico para rendimiento

No quiero presentarme como un gran experto, pero mi impresión es que, en general, el subprocesamiento múltiple más práctico para el rendimiento que ocurre en estos días es buscar lugares en una aplicación que tengan paralelismo trivial y usar múltiples hilos para cosechar los beneficios.

Al igual que con cualquier optimización, generalmente es mejor optimizar después de haber perfilado el rendimiento de su programa e identificado los puntos críticos: es fácil ralentizar un programa al decidir arbitrariamente que esta parte debe ejecutarse en un hilo y esa parte en otro, sin Primero determinar si ambas partes están ocupando una parte significativa del tiempo de CPU.

Un subproceso adicional significa más costos de configuración / desmontaje, y más cambios de contexto o más costos de comunicación entre CPU. Si no está haciendo suficiente trabajo para compensar esos costos si está en una CPU separada, y no necesita ser un hilo separado por razones de capacidad de respuesta, ralentizará las cosas sin ningún beneficio.

Busque tareas que tengan pocas interdependencias y que estén ocupando una parte significativa del tiempo de ejecución de su programa.

Si no tienen interdependencias, entonces es un caso de paralelismo trivial, puede configurar fácilmente cada uno con un hilo y disfrutar de los beneficios.

Si puede encontrar tareas con una interdependencia limitada, de modo que el bloqueo y la sincronización para intercambiar información no las ralentice significativamente, entonces el subprocesamiento múltiple puede acelerar un poco, siempre que tenga cuidado de evitar los peligros de un punto muerto debido a una lógica defectuosa al sincronizar o resultados incorrectos debido a la falta de sincronización cuando es necesario.

Alternativamente, algunas de las aplicaciones más comunes para subprocesamiento múltiple no buscan (en cierto sentido) acelerar un algoritmo predeterminado, sino un presupuesto mayor para el algoritmo que planean escribir: si está escribiendo un motor de juego , y su IA tiene que tomar una decisión dentro de su velocidad de cuadros, a menudo puede darle a su IA un presupuesto de ciclo de CPU más grande si puede darle su propia CPU.

Sin embargo, asegúrese de perfilar los hilos y asegurarse de que estén haciendo suficiente trabajo para compensar el costo en algún momento.

Algoritmos Paralelos

También hay muchos problemas que pueden acelerarse utilizando múltiples procesadores, pero que son demasiado monolíticos para simplemente dividirse entre las CPU.

Los algoritmos paralelos deben analizarse cuidadosamente para determinar sus tiempos de ejecución Big-O con respecto al mejor algoritmo no paralelo disponible, ya que es muy fácil para el costo de comunicación entre CPU eliminar cualquier beneficio del uso de múltiples CPU. En general, deben usar menos comunicación entre CPU (en términos de O grande) de lo que usan los cálculos en cada CPU.

Por el momento, sigue siendo en gran medida un espacio para la investigación académica, en parte debido al complejo análisis requerido, en parte porque el paralelismo trivial es bastante común, en parte porque todavía no tenemos tantos núcleos de CPU en nuestras computadoras que problemas no puede resolverse en un período de tiempo razonable en una CPU, podría resolverse en un período de tiempo razonable utilizando todas nuestras CPU.


+1 para una respuesta obviamente bien pensada. Sin embargo, me gustaría tener precaución para tomar las sugerencias de Microsoft al pie de la letra. Tenga en cuenta que .NET es una plataforma síncrona primero, por lo tanto, el ecosistema está sesgado para proporcionar mejores instalaciones / documentación que respalden la creación de soluciones síncronas. Lo contrario sería cierto para una primera plataforma asíncrona como Node.js.
Evan Plaice

3

la aplicación no responde y es lenta y desagradable.

Y ahí está tu problema. Una interfaz de usuario receptiva no crea una aplicación eficaz. A menudo lo contrario. Se dedica mucho tiempo a verificar la entrada de la interfaz de usuario en lugar de hacer que los hilos de trabajo hagan su trabajo.

En cuanto a 'solo' tener un enfoque asíncrono, eso también es multiproceso, aunque ajustado para ese caso de uso en particular en la mayoría de los entornos . En otros, esa sincronización se realiza a través de rutinas que ... no siempre son concurrentes.

Francamente, creo que las operaciones asincrónicas son más difíciles de razonar y usar de una manera que realmente proporciona beneficios (rendimiento, solidez, mantenibilidad) incluso en comparación con ... más enfoques manuales.


por qué ? por ejemplo, ¿qué encuentras de plátanos en la biblioteca de señales de impulso2?
user1849534
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.