Máquinas de estado vs hilos

24

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?

Victor Sorokin
fuente
44
Una computadora también es una máquina de Turing. Sin embargo, no es necesariamente útil programarlo como una máquina de Turing. La pila en lenguajes imperativos es muy útil, y los programas multiproceso permiten mantener múltiples pilas en la memoria al mismo tiempo. Hacer lo mismo en una máquina de estado seguramente es posible, pero aún más complicado.
thiton
10
Alan era un desarrollador de kernel OS; Este era su domian . Entonces su cita debe tomarse en ese contexto. Habría estado programando 'contra el metal' donde tiene más sentido usar un modelo así. Una vez que el sistema operativo extrae el hardware y sus propiedades intrínsecas (que "una computadora es una máquina de estado ...") tiene la oportunidad y el beneficio de poder usar otros modelos que tienen más sentido en su dominio . Casi todos los juegos hacen un uso intensivo de las máquinas de estado.
Steven Evers
2
El enhebrado es simplemente una característica del sistema operativo para administrar automáticamente algunos de los interruptores de su máquina de estado, si lo desea. Obviamente, puede crear una gran máquina de estados que gestionará todo por sí misma, pero eso es más complicado. Lo mismo puede decirse de los procesos. Puede decir que los procesos son para personas que tampoco pueden programar máquinas de estado. Pero la abstracción le proporciona una interfaz mucho más simple y menos propensa a errores. En mi opinión, esta es solo otra "cita genial" que debería escucharse, contemplarse y luego ignorarse en la realidad.
Yam Marcovic
2
"Pero la abstracción [thread] le proporciona una interfaz mucho más simple y menos propensa a errores". Eso parece ser falso. El número de personas que se equivocan en la seguridad del hilo indica que parece causar errores.
S.Lott
1
Muchos de los comentarios y respuestas aquí interpretan la cita como anti-multitarea en general; Creo que Alan Cox es simplemente anti-hilos y recomendaría el uso de múltiples procesos para lograr muchos de los objetivos para los que las personas usan hilos. Tenga en cuenta que es un hacker de Unix: fork FTW. No he encontrado ningún comentario de él directamente en la cita, pero aquí hay uno de Larry McVoy de la lista de correo del kernel de Linux que va en esta dirección: lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
Martin B

Respuestas:

25

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.

  • T1-antes de bloqueo
  • T1-con cerradura
  • T1-después del bloqueo
  • T2-antes de bloqueo
  • T2-con cerradura
  • T2-after lock

[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.

S.Lott
fuente
Gracias, buena explicación. ¿Y qué hay de la máquina multinúcleo? Supongo que no hay una forma explícita de explotar los núcleos en la máquina de estado Java. Uno necesita confiar en el sistema operativo para eso, ¿verdad?
Victor Sorokin
55
Una máquina multinúcleo hace que la programación sea un poco más compleja. Sin embargo, todos los núcleos escriben en una única memoria común, por lo que el Orden de escritura de memoria entre los dos núcleos significa que, esencialmente, volvemos a la ejecución intercalada de escrituras en memoria. El sistema operativo explota los núcleos y la JVM aprovecha esto. No hay necesidad de pensarlo dos veces.
S.Lott
9

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.

SF.
fuente
Una falla de página de códigos en un programa de contexto único de ejecución es fundamentalmente un bloqueo. Por definición, el código que tiene un solo contexto de ejecución no puede avanzar hasta que esté disponible el siguiente fragmento de código en el flujo.
David Schwartz
1
@David Schwartz: cierto, es fundamentalmente un bloqueo; pero no es una 'operación' ya que no es algo que hace el código bloqueado, es algo que le sucede.
Javier
1
La lectura de archivos no es un bloqueo fundamental: siempre se puede dividir en solicitar la lectura de una ubicación determinada y adquirir los datos de los buffers después de que se haya completado la solicitud. Y una falla de página es una solución alternativa para el uso de intercambio aleatorio / heurístico. Ocurrirá si se ingresó un estado dado antes de que todos los datos necesarios para su ejecución estuvieran disponibles, una falta de previsión, algo en contra del concepto de máquina de estados. Si las operaciones de intercambio e intercambio forman parte de la máquina de estado, no se producirá un error de página.
SF.
1
@David Schwartz: La definición de comportamiento de "bloqueo" siempre está sujeta a requisitos de "tiempo real". Por ejemplo: la falla de la página de códigos se considera sin bloqueo para la aplicación que necesita capacidad de respuesta en el orden de cientos de milisegundos. Por otro lado, si la aplicación tiene requisitos estrictos en tiempo real, no usaría memoria virtual en absoluto.
MaR
1
@Mar: ... o use un algoritmo de intercambio determinista que garantice la obtención de los datos requeridos antes de que sean necesarios.
SF.
9

¿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?

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.

¿Está usando la alternativa "solo máquina de estado" como alternativa viable al multihilo en lenguajes de alto nivel?

¿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.

Blrfl
fuente
2

¿Qué pasa si hay 2 actividades para realizar (hacer cálculos y hacer E / S) y una actividad puede bloquear?

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:

De: Alan Cox ([email protected])
Fecha: viernes 21 de enero de 2000 - 13:33:52 EST

el documento de IBM), que si su aplicación depende de un gran número de subprocesos, ¿siempre se encontrará con el planificador? mucha gente arroja muchos hilos a un problema y realmente puede ser un mal diseño.

Esa es la menor de tus preocupaciones. 1000 subprocesos son 8Mb de pilas de kernel, y suficiente cambio de tareas para asegurarse de que también podría apagar la mayor parte de su caché. Una computadora es una máquina de estado. Los hilos son para personas que no pueden programar máquinas de estado.

Hay muchos casos en que Linux definitivamente no está ayudando a la situación, especialmente la E / S de bloque asíncrono.

Alan

fuente: Re: Análisis interesante de subprocesamiento de kernel de Linux por IBM (archivos de la lista de correo de kernel de Linux)

mosquito
fuente
1
  • 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.

Mouviciel
fuente
1

Los hilos son la única opción en dos casos:

  • usar múltiples núcleos sin separación de memoria.
  • para hacer frente al código externo que bloquea.

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.

mbq
fuente
1
Lo mejor de ambos mundos: las máquinas de estado pequeñas y de bloqueo en subprocesos hacen que el código de la aplicación sea muy simple. Y los hilos se dividen muy bien en núcleos si los tienes. Si todo no tiene al menos dos núcleos, lo tendrá pronto. es decir, los teléfonos basados en quad brazo núcleo procedente 2012.
Tim Williscroft
0

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:

while (true) {
    switch (event) {
        case ButtonPressed:
        ...
        case MachineIsBurning:
        ....
    }
}

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.

thiton
fuente
0

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.

Mark Booth
fuente
0

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í.

gbjbaanb
fuente
2
¡Las máquinas de estado no son complicadas en absoluto! Las máquinas de estado complejas son complicadas, pero también lo son todos los sistemas complejos: o)
MaR
2
-1 para "no intentes". ese es el peor consejo que puedes dar.
Javier
1
-1 "No lo intentes"? Eso es una tontería. También desafiaría su afirmación de que las máquinas de estado son difíciles. Una vez que te encuentras en algo así como una máquina de estado borrosa Heirarchal ... entonces sí, es un poco más complicado. ¿Pero una simple máquina de estados? Eso es algo bastante básico que cada segundo año aprendí cuando estaba en la escuela.
Steven Evers
déjeme reformular el 'No intentar' poco ...
gbjbaanb
0

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.

ZeusTheTrueDios
fuente