Alan Cox dijo una vez : "Una computadora es una máquina de estado. Los hilos son para personas que no pueden programar máquinas de estado".
Dado que preguntarle directamente a Alan no es una opción para humillarme, prefiero preguntar aquí: ¿cómo se logra la funcionalidad de subprocesos múltiples en un lenguaje de alto nivel, como Java, utilizando solo un subproceso y una máquina de estado? Por ejemplo, ¿qué pasa si hay 2 actividades para realizar (hacer cálculos y hacer E / S) y una actividad puede bloquear?
¿Está utilizando la alternativa "solo máquina de estado" de manera viable al subprocesamiento múltiple en lenguajes de alto nivel?
multithreading
finite-state-machine
Victor Sorokin
fuente
fuente
Respuestas:
Todo lo que hace un hilo es intercalar operaciones para que partes del proceso parezcan superponerse en el tiempo. Una máquina de un solo núcleo con múltiples subprocesos simplemente salta: ejecuta pequeños bits de código de un subproceso, luego cambia a otro subproceso. Un planificador simple decide qué subproceso tiene la máxima prioridad y se ejecuta realmente en el núcleo.
En una computadora de un solo núcleo, en realidad no sucede nada "al mismo tiempo". Todo es solo ejecución intercalada.
Hay muchas, muchas formas de lograr el entrelazado. Muchos.
Supongamos que tiene un proceso simple de dos subprocesos que utiliza un bloqueo simple para que ambos subprocesos puedan escribir en una variable común. Tienes seis bloques de código.
[Esto puede estar en un bucle o tener más bloqueos o lo que sea. Todo lo que hace es alargarse, no ser más complejo.]
Los pasos de T1 deben ejecutarse en orden (T1-antes, T1-con, T1-después) y los pasos de T2 deben ejecutarse en orden (T2-antes, T2-con, T2-después).
Aparte de la restricción "en orden", estos se pueden intercalar de cualquier manera. De todas formas. Podrían ejecutarse como se indica arriba. Otro pedido válido es (T1-before, T2-before, T2-lock, T1-lock, T2-after, T1-after). Hay muchos pedidos válidos.
Espere.
Esto es solo una máquina de estados con seis estados.
Es un autómata de estado finito no determinista. El orden de los estados T1-xxx con estados T2-xxx es indeterminado y no importa. Así que hay lugares donde el "próximo estado" es un lanzamiento de moneda.
Por ejemplo, cuando comienza el FSM, T1-before o T2-before son los dos primeros estados legítimos. Tirar una moneda.
Digamos que surgió T1-antes. Haz eso. Cuando se hace eso, hay una opción entre T1-with y T2-before. Tirar una moneda.
En cada paso en el FSM habrá dos opciones (dos hilos - dos opciones) y un lanzamiento de moneda puede determinar qué estado específico se sigue.
fuente
Escribir funciones de bloqueo es para personas que no pueden crear máquinas de estado;)
Los hilos son útiles si no puede evitar el bloqueo. Ninguna actividad informática fundamental está realmente bloqueando, es solo que muchas de ellas se implementan de esa manera para facilitar su uso. En lugar de devolver un carácter o "error de lectura", una función de lectura se bloquea hasta que se lee todo el búfer. En lugar de verificar el mensaje de retorno en una cola, y regresar si no se encuentra ninguno, una función de conexión espera respuesta.
No puede usar funciones de bloqueo en una máquina de estado (al menos una que no se puede "congelar").
Y sí, usar la máquina de estado es una alternativa viable. En los sistemas de tiempo real, esta es la única opción, el sistema proporciona un marco para la máquina. Usar subprocesos y funciones de bloqueo es solo "la salida fácil", porque generalmente una llamada a una función de bloqueo reemplaza aproximadamente 3-4 estados en la máquina de estados.
fuente
Lo que está describiendo se llama multitarea cooperativa , donde las tareas se asignan a la CPU y se espera que renuncien voluntariamente después de una cantidad de tiempo o actividad autodeterminada. Una tarea que no coopera al continuar usando la CPU o al bloquear las encías en todo el trabajo y sin tener un temporizador de vigilancia de hardware, no hay nada que el código que supervise las tareas pueda hacer al respecto.
Lo que ves en los sistemas modernos se llama multitarea preventiva , que es donde las tareas no tienen que renunciar a la CPU porque el supervisor lo hace por ellas cuando llega una interrupción generada por hardware. La rutina de servicio de interrupción en el supervisor guarda el estado de la CPU y la restaura la próxima vez que se considere que la tarea merece un intervalo de tiempo, luego restaura el estado de la tarea que se ejecutará a continuación y vuelve a saltar como si nada hubiera sucedido . Esta acción se llama cambio de contexto y puede ser costosa.
¿Viable? Seguro. ¿Cuerdo? A veces. Si usa hilos o alguna forma de multitarea cooperativa casera (por ejemplo, máquinas de estado) depende de las compensaciones que esté dispuesto a hacer.
Los subprocesos simplifican el diseño de tareas hasta el punto en que puede tratar a cada uno como su propio programa que comparte espacio de datos con otros. Esto le da la libertad de concentrarse en el trabajo en cuestión y no toda la administración y el servicio de limpieza necesarios para que funcione una iteración a la vez. Pero como ninguna buena acción queda impune, usted paga por toda esta conveniencia en los cambios de contexto. Tener muchos subprocesos que producen la CPU después de hacer un trabajo mínimo (voluntariamente o haciendo algo que se bloquearía, como E / S) puede consumir mucho tiempo del procesador al cambiar de contexto. Esto es especialmente cierto si sus operaciones de bloqueo raramente bloquean por mucho tiempo.
Hay algunas situaciones en las que la ruta cooperativa tiene más sentido. Una vez tuve que escribir algún software de usuario para una pieza de hardware que transmitía muchos canales de datos a través de una interfaz mapeada en memoria que requería sondeo. Cada canal era un objeto construido de tal manera que podía dejarlo correr como un hilo o ejecutar repetidamente un solo ciclo de sondeo.
El rendimiento de la versión multiproceso no fue bueno en absoluto por exactamente la razón que describí anteriormente: cada hilo estaba haciendo un trabajo mínimo y luego producía la CPU para que los otros canales pudieran tener algo de tiempo, causando muchos cambios de contexto. Permitir que los subprocesos se ejecuten libremente hasta evitarlos ayudó con el rendimiento, pero resultó en que algunos canales no recibieran servicio antes de que el hardware experimentara un desbordamiento del búfer porque no obtuvieron un intervalo de tiempo lo suficientemente pronto.
La versión de un solo subproceso, que hizo incluso iteraciones de cada canal, corrió como un simio escaldado y la carga en el sistema cayó como una roca. La penalización que pagué por el rendimiento adicional fue tener que hacer malabarismos con las tareas yo mismo. En este caso, el código para hacerlo fue lo suficientemente simple como para que el costo de desarrollarlo y mantenerlo valiera la pena para mejorar el rendimiento. Supongo que es realmente el resultado final. Si mis hilos hubieran estado esperando a que volviera alguna llamada del sistema, el ejercicio probablemente no hubiera valido la pena.
Eso me lleva al comentario de Cox: los hilos no son exclusivamente para personas que no pueden escribir máquinas de estado. Algunas personas son bastante capaces de hacerlo, pero eligen usar una máquina de estados enlatada (es decir, un hilo) con el interés de hacer el trabajo antes o con menos complejidad.
fuente
Bueno, sinceramente, no puedo imaginar cómo manejar el bloqueo de E / S sin hilos. Se llama bloqueo después de todo solo porque el código que lo invoca tiene que hacerlo
wait
.Según mi lectura del correo electrónico original de Cox (a continuación), señala que los hilos no se adaptan bien. Quiero decir, ¿qué pasa si hay 100 solicitudes de E / S? 1000? 10000? Cox señala que tener una gran cantidad de subprocesos puede provocar problemas graves:
fuente: Re: Análisis interesante de subprocesamiento de kernel de Linux por IBM (archivos de la lista de correo de kernel de Linux)
fuente
En teoría, esto es cierto. En la vida real, los subprocesos son solo una abstracción eficiente utilizada para programar una máquina de estado de este tipo. Son tan eficientes que pueden usarse para programar gráficos de estado y redes de Petri también (es decir, comportamientos paralelos, donde las máquinas de estado son básicamente secuenciales).
El problema con las máquinas de estado es la explosión combinatoria. El número de estados de una computadora con 4G RAM es de 2 ^ (2 ^ 32) estados (sin contar la unidad de disco 2T).
Para un hombre cuya única herramienta es un martillo, cada problema parece un clavo.
fuente
Los hilos son la única opción en dos casos:
El segundo es por qué la mayoría de la gente piensa que los hilos son inevitables para hacer IO o programación de red, pero esto generalmente se debe a que no saben que su sistema operativo tiene una API más avanzada (o no quieren luchar con su uso).
En cuanto a la facilidad de uso y la legibilidad, siempre hay bucles de eventos (como libev o EventMachine ) que hacen que programar una máquina de estados sea casi tan simple como hacerlo con subprocesos, pero que proporciona el control suficiente para olvidarse de los problemas de sincronización.
fuente
Una buena forma de asimilar la forma en que interactúan las máquinas de estado y los subprocesos múltiples es mirar los controladores de eventos de la GUI. Muchas aplicaciones / framework GUI utilizan un único hilo GUI que sondeará las posibles fuentes de entrada y llamará a una función para cada entrada recibida; esencialmente, esto podría escribirse como un gran cambio:
Ahora, queda claro con bastante rapidez que el nivel de control de alto nivel en esta construcción no puede ser alto: el controlador para ButtonPressed debe finalizar sin interacción del usuario y volver al bucle principal, porque si no lo hace, no habrá más eventos de usuario Puede ser procesado. Si tiene algún estado para guardar, este estado debe guardarse en variables globales o estáticas, pero no en la pila; es decir, el flujo de control normal en un lenguaje imperativo es limitado. Estás esencialmente limitado a una máquina de estados.
Esto puede volverse bastante complicado cuando tiene subrutinas anidadas que tienen que guardar, por ejemplo, un nivel de recursión. O están en medio de la lectura de un archivo, pero el archivo no está disponible en este momento. O simplemente están en un largo cálculo. En todos estos casos, es deseable guardar el estado de la ejecución actual y volver al bucle principal, y esto es multihilo . Nada más y nada menos.
Todo se volvió un poco más complicado con la introducción de subprocesamiento múltiple preventivo (es decir, el sistema operativo decide cuándo los hilos deberían ceder el control), y es por eso que la conexión no está clara de inmediato hoy.
Entonces, para responder la pregunta final: Sí, la máquina de estado es una alternativa, la mayoría de las GUI funcionan de esa manera con el hilo de la GUI. Simplemente no empuje la máquina de estados demasiado, se vuelve imposible de mantener realmente rápido.
fuente
Preguntar si el uso de una máquina de estado es viable en un lenguaje de alto nivel es como preguntar si escribir en ensamblador es una alternativa viable al uso de un lenguaje de alto nivel. Ambos tienen su lugar, dada la situación correcta.
La abstracción del uso de subprocesos hace que los sistemas paralelos más complejos sean más fáciles de implementar, pero en última instancia, todos los sistemas paralelos tienen los mismos problemas con los que lidiar. Los problemas clásicos como Deadlock / Livelock e inversión de prioridad son tan posibles con sistemas basados en máquinas de estado como con un sistema paralelo de memoria compartida , NUMA o incluso CSP , si es lo suficientemente complejo.
fuente
No creo que lo sea, claro, las máquinas de estado son un concepto informático muy "elegante" pero, como usted dice, son bastante complicadas. Y las cosas complicadas son difíciles de corregir. Y las cosas que no están bien simplemente están rotas, así que a menos que seas un genio de la presunta estatura de Alan Cox, quédate con cosas que sabes que funcionan: deja la 'codificación inteligente' para los proyectos de aprendizaje.
Puede saber cuándo alguien ha intentado en vano hacer uno, ya que (suponiendo que funcione bien) cuando se trata de mantenerlo, descubre que la tarea es casi imposible. El 'genio' original se habrá movido dejándote con la cantidad de código apenas comprensible (ya que este tipo de desarrolladores no tienden a dejar demasiados comentarios y mucho menos documentación técnica).
En algunos casos, una máquina de estado será una mejor opción: estoy pensando en cosas de tipo incrustado ahora donde se usan algunos patrones de máquina de estado y se usan repetidamente y de una manera más formal (es decir, ingeniería adecuada :))
Los subprocesos también pueden ser difíciles de entender, pero existen patrones para ayudarlo, principalmente al reducir la necesidad de compartir datos entre los subprocesos.
El último punto acerca de esto es que las computadoras modernas se ejecutan en muchos núcleos de todos modos, por lo que una máquina de estado realmente no aprovechará los recursos disponibles. Enhebrar puede hacer un mejor trabajo aquí.
fuente
Buen ejemplo de un uso adecuado de la máquina de estado en lugar de hilos: nginx vs apache2. Generalmente puede suponer que nginx maneja todas las conexiones en un hilo, apache2 crea un hilo por conexión.
Pero para mí el uso de máquinas de estado vs hilos es bastante similar al uso de asm perfectamente hecho a mano vs Java: puede lograr resultados increibles, pero requiere muchos esfuerzos de los programadores, mucha disciplina, hacer que el proyecto sea más complejo y valioso solo cuando lo usa muchos otros programadores. Entonces, si usted es el que quiere hacer un servidor web rápido, use máquinas de estado y async IO. Si está escribiendo el proyecto (no la biblioteca que se usará en todas partes), use hilos.
fuente