multithreading - entre - programa de hilos en python
¿Cuántos hilos o tan pocos hilos como sea posible? (5)
Creo que la pregunta que debería hacerse no es si el número 200 como hilo general es bueno o malo, sino cuántos de esos hilos van a estar activos.
Si solo varios de ellos están activos en un momento dado, mientras todos los demás están durmiendo o esperando o lo que sea, entonces estás bien. Los hilos para dormir, en este contexto, no le cuestan nada.
Sin embargo, si todos esos 200 hilos están activos, tu CPU perderá tanto tiempo haciendo cambios de contexto entre todos esos ~ 200 hilos.
Como proyecto paralelo, actualmente estoy escribiendo un servidor para un viejo juego que solía jugar. Estoy tratando de hacer que el servidor esté lo más unido posible, pero me pregunto cuál sería una buena decisión de diseño para multihilo. Actualmente tengo la siguiente secuencia de acciones:
- Inicio (crea) ->
- Servidor (escucha para los clientes, crea) ->
- Cliente (escucha los comandos y envía datos de período)
Asumo un promedio de 100 clientes, ya que ese era el máximo en cualquier momento del juego. ¿Cuál sería la decisión correcta en cuanto a enhebrar todo? Mi configuración actual es la siguiente:
- 1 hilo en el servidor que escucha nuevas conexiones, en una nueva conexión crea un objeto cliente y comienza a escuchar nuevamente.
- El objeto cliente tiene un hilo, escucha los comandos entrantes y envía datos periódicos. Esto se hace usando un socket no bloqueante, por lo que simplemente verifica si hay datos disponibles, se ocupa de eso y luego envía los mensajes que ha puesto en cola. El inicio de sesión se realiza antes de que se inicie el ciclo de envío y recepción.
- Un hilo (por ahora) para el juego en sí, ya que considero que está separado de toda la parte cliente-servidor, arquitectónicamente hablando.
Esto daría como resultado un total de 102 hilos. Incluso estoy considerando darle al cliente 2 hilos, uno para enviar y otro para recibir. Si hago eso, puedo usar E / S de bloqueo en el hilo del receptor, lo que significa que el hilo estará prácticamente inactivo en una situación promedio.
Mi principal preocupación es que al usar tantos hilos estaré acaparando recursos. No me preocupan las condiciones de carrera ni los bloqueos, ya que es algo de lo que tendré que ocuparme.
Mi diseño está configurado de tal manera que podría usar un solo hilo para todas las comunicaciones del cliente, sin importar si es 1 o 100. He separado la lógica de comunicaciones del objeto del cliente en sí, para poder implementarlo sin tener que reescribir un montón de código
La pregunta principal es: ¿está mal usar más de 200 hilos en una aplicación? ¿Tiene ventajas? Estoy pensando en ejecutar esto en una máquina multi-core, ¿aprovecharía mucho la ventaja de núcleos múltiples como este?
¡Gracias!
De todos estos hilos, la mayoría de ellos serán bloqueados por lo general. No espero conexiones de más de 5 por minuto. Los comandos del cliente aparecerán con poca frecuencia, yo diría 20 por minuto en promedio.
Siguiendo las respuestas que recibo aquí (el cambio de contexto fue el golpe de rendimiento en el que estaba pensando, pero no lo supe hasta que lo señalaste, ¡gracias!) Creo que elegiré el enfoque con un oyente, uno receptor, un remitente y algunas cosas misceláneas ;-)
Escribo en .NET y no estoy seguro si la forma en que el código se debe a las limitaciones de .NET y su diseño de API o si esta es una forma estándar de hacer las cosas, pero así es como he hecho este tipo de cosas en el pasado:
Un objeto de cola que se usará para procesar datos entrantes. Esto se debe sincronizar bloqueado entre el hilo de cola y el hilo de trabajo para evitar condiciones de carrera.
Un hilo de trabajo para procesar datos en la cola. El hilo que pone en cola la cola de datos usa semáforo para notificar a este hilo que procese elementos en la cola. Este subproceso se iniciará antes que cualquiera de los otros subprocesos y contendrá un bucle continuo que se puede ejecutar hasta que reciba una solicitud de cierre. La primera instrucción en el ciclo es un indicador para pausar / continuar / terminar el procesamiento. Inicialmente, la bandera se configurará en pausa para que el hilo se encuentre en un estado inactivo (en lugar de un bucle continuo) mientras no haya procesamiento por hacer. El hilo de cola cambiará el indicador cuando haya elementos en la cola para ser procesados. Este subproceso procesará un único elemento en la cola en cada iteración del ciclo. Cuando la cola está vacía, volverá a colocar la bandera en pausa para que en la siguiente iteración del ciclo espere hasta que el proceso de colas le notifique que hay más trabajo por hacer.
Un hilo de escucha de conexión que escucha las solicitudes de conexión entrantes y las pasa a ...
Un hilo de procesamiento de conexión que crea la conexión / sesión. Tener un hilo separado de su hilo de escucha de conexión significa que está reduciendo el potencial de solicitudes de conexión perdidas debido a la reducción de recursos mientras ese hilo está procesando solicitudes.
Un hilo de escucha entrante de datos que escucha los datos entrantes en la conexión actual. Todos los datos pasan a un hilo de cola para ser puestos en cola para su procesamiento. Sus hilos de escucha deben hacer lo menos posible fuera de la escucha básica y pasar los datos para su procesamiento.
Un hilo de cola que pone en cola los datos en el orden correcto para que todo se pueda procesar correctamente, este subproceso eleva el semáforo a la cola de procesamiento para informarle que hay datos para procesar. Tener este hilo separado del oyente de datos entrantes significa que es menos probable que pierda datos entrantes.
Algún objeto de sesión que se pasa entre los métodos para que la sesión de cada usuario sea independiente en todo el modelo de subprocesamiento.
Esto hace que los hilos se reduzcan a un modelo simple pero robusto como lo he descubierto. Me encantaría encontrar un modelo más simple que este, pero descubrí que si trato de reducir el modelo de subprocesamiento, empiezo a perder datos en el flujo de red o pierdo las solicitudes de conexión.
También ayuda con TDD (Desarrollo controlado por prueba) de modo que cada subproceso está procesando una única tarea y es mucho más fácil codificar las pruebas. Tener cientos de hilos puede convertirse rápidamente en una pesadilla de asignación de recursos, mientras que tener un único hilo se convierte en una pesadilla de mantenimiento.
Es mucho más simple mantener un hilo por tarea lógica de la misma manera que tendría un método por tarea en un entorno TDD y puede separar lógicamente lo que cada uno debería hacer. Es más fácil detectar problemas potenciales y mucho más fácil solucionarlos.
Para responder la pregunta simplemente, es completamente incorrecto usar 200 hilos en el hardware de hoy.
Cada hilo ocupa 1 MB de memoria, por lo que está tomando 200 MB de archivo de página antes de comenzar a hacer algo útil.
Por supuesto, divida sus operaciones en pequeños fragmentos que puedan ejecutarse con seguridad en cualquier hilo, pero coloque esas operaciones en colas y tenga un número fijo y limitado de hilos de trabajo para esas colas.
Actualización: ¿desperdicia 200MB de materia? En una máquina de 32 bits, es el 10% de todo el espacio teórico de direcciones para un proceso, sin más preguntas. En una máquina de 64 bits, suena como una gota en el océano de lo que podría estar disponible teóricamente, pero en la práctica sigue siendo un trozo muy grande (o más bien, una gran cantidad de trozos bastante grandes) de almacenamiento que el aplicación, y que luego tiene que ser administrado por el sistema operativo. Tiene el efecto de rodear la valiosa información de cada cliente con un montón de relleno inútil, que destruye la localidad, derrotando los intentos del sistema operativo y de la CPU para mantener las cosas de acceso frecuente en las capas más rápidas de caché.
En cualquier caso, el desperdicio de memoria es solo una parte de la locura. A menos que tenga 200 núcleos (y un SO capaz de utilizar), entonces realmente no tiene 200 hilos paralelos. Tiene (digamos) 8 núcleos, cada uno alternando frenéticamente entre 25 hilos. Ingenuamente, podrías pensar que como resultado de esto, cada hilo experimenta el equivalente de correr en un núcleo que es 25 veces más lento. Pero en realidad es mucho peor que eso: el sistema operativo pasa más tiempo quitando un hilo de un núcleo y colocando otro en él ("cambio de contexto") que lo que realmente permite que se ejecute su código.
Simplemente observe cómo cualquier diseño exitoso bien conocido aborda este tipo de problema. El grupo de hilos del CLR (incluso si no lo está usando) sirve como un buen ejemplo. Comienza suponiendo que solo un hilo por núcleo será suficiente. Permite crear más, pero solo para garantizar que los algoritmos paralelos mal diseñados finalmente se completen. Se niega a crear más de 2 subprocesos por segundo, por lo que castiga eficazmente los algoritmos codiciosos de subprocesos ralentizándolos.
utilizar un flujo de eventos / cola y un grupo de hilos para mantener el equilibrio; esto se adaptará mejor a otras máquinas que pueden tener más o menos núcleos
en general, muchos más hilos activos que los núcleos perderán el tiempo de cambio de contexto
si su juego consiste en muchas acciones cortas, una cola de eventos circulares / de reciclaje dará un mejor rendimiento que una cantidad fija de hilos
¿Cuál es tu plataforma? Si Windows, entonces sugeriría mirar operaciones asincrónicas y grupos de subprocesos (o Puertos de finalización de E / S directamente si está trabajando en el nivel de la API de Win32 en C / C ++).
La idea es que tenga un pequeño número de subprocesos que se ocupen de su E / S y esto hace que su sistema sea capaz de escalar a un gran número de conexiones simultáneas porque no hay relación entre la cantidad de conexiones y el número de subprocesos utilizados por el proceso. eso los está sirviendo Como era de esperar, .Net lo aísla de los detalles y Win32 no lo hace.
El desafío de usar asincronización de E / S y este estilo de servidor es que el procesamiento de las solicitudes de los clientes se convierte en una máquina de estados en el servidor y los datos que llegan provocan cambios de estado. A veces esto lleva un tiempo acostumbrarse, pero una vez que lo haces, es bastante maravilloso;)
Tengo un código gratuito que muestra varios diseños de servidor en C ++ usando IOCP aquí .
Si está usando Unix o necesita ser multiplataforma y está en C ++, le recomendamos que consulte el aumento de ASIO que proporciona la funcionalidad de E / S asíncrona.