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), async
se utiliza un método, que await
es 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, await
significa 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.