Estoy elaborando un pequeño proyecto en C y C ++ mixtos. Estoy construyendo una máquina de estado pequeña en el corazón de uno de mis hilos de trabajo.
Me preguntaba si ustedes, los gurús de SO, compartirían sus técnicas de diseño de máquinas de estado.
NOTA: Estoy principalmente después de técnicas de implementación probadas.
ACTUALIZADO: Basado en toda la gran aportación recopilada en SO, me he decidido por esta arquitectura:
c++
c
architecture
state-machine
jldupont
fuente
fuente
Respuestas:
Las máquinas de estado que he diseñado antes (C, no C ++) se han reducido a una
struct
matriz y un bucle. La estructura consiste básicamente en un estado y evento (para búsqueda) y una función que devuelve el nuevo estado, algo así como:Luego, define sus estados y eventos con definiciones simples (los
ANY
que son marcadores especiales, ver más abajo):Luego define todas las funciones que son llamadas por las transiciones:
Todas estas funciones están escritas para no tomar variables y devolver el nuevo estado para la máquina de estados. En este ejemplo, las variables globales se utilizan para pasar cualquier información a las funciones de estado cuando sea necesario.
Usar globals no es tan malo como parece, ya que el FSM generalmente está encerrado dentro de una sola unidad de compilación y todas las variables son estáticas en esa unidad (es por eso que usé citas alrededor de "global" arriba, son más compartidas dentro del FSM, que verdaderamente global). Como con todos los globales, requiere cuidado.
La matriz de transiciones luego define todas las transiciones posibles y las funciones que se llaman para esas transiciones (incluida la última captura general):
Lo que eso significa es: si está en el
ST_INIT
estado y recibe elEV_KEYPRESS
evento, llame alGotKey
.El funcionamiento del FSM se convierte en un ciclo relativamente simple:
Como se mencionó anteriormente, tenga en cuenta el uso de
ST_ANY
como comodines, lo que permite que un evento llame a una función sin importar el estado actual.EV_ANY
también funciona de manera similar, permitiendo que cualquier evento en un estado específico llame a una función.También puede garantizar que, si llega al final de la matriz de transiciones, obtiene un error que indica que su FSM no se ha creado correctamente (mediante el uso de la
ST_ANY/EV_ANY
combinación.He utilizado un código similar para esto en muchos proyectos de comunicaciones, como una implementación temprana de pilas de comunicaciones y protocolos para sistemas integrados. La gran ventaja era su simplicidad y relativa facilidad para cambiar la matriz de transiciones.
No tengo dudas de que habrá abstracciones de nivel superior que pueden ser más adecuadas hoy en día, pero sospecho que todas se reducirán a este mismo tipo de estructura.
Y como
ldog
dice en un comentario, puede evitar las variables globales pasando un puntero de estructura a todas las funciones (y usándolo en el bucle de eventos). Esto permitirá que varias máquinas de estado funcionen una al lado de la otra sin interferencia.Simplemente cree un tipo de estructura que contenga los datos específicos de la máquina (estado como mínimo) y úselos en lugar de los globales.
La razón por la que rara vez lo he hecho es simplemente porque la mayoría de las máquinas de estado que he escrito han sido de tipo único (por ejemplo, al inicio del proceso, lectura de archivos de configuración, por ejemplo), sin necesidad de ejecutar más de una instancia . Pero tiene valor si necesita ejecutar más de uno.
fuente
int (*fn)(void*);
dondevoid*
está el puntero a los datos que cada función de estado toma como parámetro. Entonces las funciones de estado pueden usar los datos o ignorarlos.Las otras respuestas son buenas, pero una implementación muy "ligera" que he usado cuando la máquina de estados es muy simple parece:
Lo usaría cuando la máquina de estado sea lo suficientemente simple como para que el puntero de función y el enfoque de la tabla de transición de estado sean excesivos. Esto suele ser útil para el análisis de caracteres por caracteres o de palabra por palabra.
fuente
Discúlpeme por romper todas las reglas en informática, pero una máquina de estados es uno de los pocos lugares (puedo contar solo dos), donde una
goto
declaración no solo es más eficiente, sino que también hace que su código sea más limpio y fácil de leer. Debido a que lasgoto
declaraciones se basan en etiquetas, puede nombrar sus estados en lugar de tener que hacer un seguimiento de un desorden de números o usar una enumeración. También proporciona un código mucho más limpio, ya que no necesita todos los punteros extra crudos de funciones o grandes declaraciones de cambio y bucles while. ¿Mencioné que también es más eficiente?Así es como se vería una máquina de estados:
Tienes la idea general. El punto es que puede implementar la máquina de estado de una manera eficiente y que sea relativamente fácil de leer y le grita al lector que está mirando una máquina de estado. Tenga en cuenta que si está usando
goto
declaraciones, debe tener cuidado ya que es muy fácil dispararse en el pie mientras lo hace.fuente
Puede considerar el compilador de máquinas de estado http://smc.sourceforge.net/
Esta espléndida utilidad de código abierto acepta una descripción de una máquina de estado en un lenguaje simple y la compila en cualquiera de una docena de lenguajes, incluidos C y C ++. La utilidad en sí está escrita en Java y se puede incluir como parte de una compilación.
La razón para hacer esto, en lugar de la codificación manual utilizando el patrón de estado GoF o cualquier otro enfoque, es que una vez que su máquina de estados se expresa como código, la estructura subyacente tiende a desaparecer bajo el peso de repetitivo que debe generarse para soportarlo. El uso de este enfoque le brinda una excelente separación de preocupaciones y mantiene la estructura de su máquina de estado 'visible'. El código generado automáticamente entra en módulos que no necesita tocar, para que pueda retroceder y manipular la estructura de la máquina de estado sin afectar el código de soporte que ha escrito.
Lo siento, estoy demasiado entusiasmado y, sin duda, estoy posponiendo a todos. Pero es una utilidad de primer nivel, y también está bien documentada.
fuente
Asegúrese de verificar el trabajo de Miro Samek (blog State Space , sitio web State Machines & Tools ), cuyos artículos en el C / C ++ Users Journal fueron geniales.
El sitio web contiene una implementación completa (C / C ++) en licencia de código abierto y comercial de un marco de máquina de estado (QP Framework) , un controlador de eventos (QEP) , una herramienta de modelado básica (QM) y una herramienta de rastreo (QSpy) que Permitir dibujar máquinas de estado, crear código y depurarlas.
El libro contiene una amplia explicación sobre qué / por qué de la implementación y cómo usarlo, y también es un excelente material para comprender los fundamentos de las máquinas de estados jerárquicos y finitos.
El sitio web también contiene enlaces a varios paquetes de soporte de la placa para el uso del software con plataformas integradas.
fuente
Hice algo similar a lo que describe paxdiablo, solo que en lugar de una matriz de transiciones de estado / evento, configuré una matriz bidimensional de punteros de función, con el valor del evento como el índice de un eje y el valor del estado actual como el otro. Entonces solo llamo
state = state_table[event][state](params)
y sucede lo correcto. Las celdas que representan combinaciones de estado / evento no válidas obtienen un puntero a una función que lo dice, por supuesto.Obviamente, esto solo funciona si los valores de estado y evento son rangos contiguos y comienzan en 0 o lo suficientemente cerca.
fuente
#define STATE_LIST() \STATE_LIST_ENTRY(state1)\STATE_LIST_ENTRY(state2)\...
(una nueva línea implícita después de cada uno\
) donde (re) defina la macro de entrada cuando use la macro STATE_LIST. Ejemplo - hacer una variedad de nombres de estado:#define STATE_LIST_ENTRY(s) #s , \n const char *state_names[] = { STATE_LIST() };\n #undef STATE_LIST_ENTRY
. Algunos trabajan para configurar primero, pero esto es extremadamente poderoso. Añadir nuevo estado -> garantizado sin fallos.Stefan Heinzmann da un muy buen "marco" de máquina de estado C ++ basado en plantillas en su artículo .
Como no hay un enlace a una descarga completa de código en el artículo, me tomé la libertad de pegar el código en un proyecto y comprobarlo. Las cosas a continuación se prueban e incluyen las pocas piezas faltantes menores pero bastante obvias.
La principal innovación aquí es que el compilador está generando código muy eficiente. Las acciones de entrada / salida vacías no tienen costo. Las acciones de entrada / salida no vacías están en línea. El compilador también está verificando la integridad del diagrama de estado. Las acciones faltantes generan errores de enlace. Lo único que no se atrapa son los desaparecidos
Top::init
.Esta es una muy buena alternativa a la implementación de Miro Samek, si puede vivir sin lo que falta: está lejos de ser una implementación completa de UML Statechart, aunque implementa correctamente la semántica de UML, mientras que el código de Samek por diseño no maneja la salida / transición / acciones de entrada en el orden correcto.
Si este código funciona para lo que necesita hacer, y tiene un compilador de C ++ decente para su sistema, probablemente funcionará mejor que la implementación de C / C ++ de Miro. El compilador genera una implementación aplanada de máquina de estado de transición O (1) para usted. Si la auditoría de la salida del ensamblaje confirma que las optimizaciones funcionan según lo deseado, se acerca al rendimiento teórico. La mejor parte: es un código relativamente pequeño y fácil de entender.
El código de prueba sigue.
fuente
La técnica que me gusta para las máquinas de estado (al menos las de control de programas) es utilizar punteros de función. Cada estado está representado por una función diferente. La función toma un símbolo de entrada y devuelve el puntero de función para el siguiente estado. Los monitores de bucle de despacho central toman la siguiente entrada, la alimentan al estado actual y procesan el resultado.
El escribir en él se vuelve un poco extraño, ya que C no tiene una manera de indicar los tipos de punteros de función que se devuelven, por lo que las funciones de estado regresan
void*
. Pero puedes hacer algo como esto:Luego, sus funciones de estado individuales pueden activar su entrada para procesar y devolver el valor apropiado.
fuente
Caso más simple
Puntos: El estado es privado, no solo para la unidad de compilación sino también para el controlador de eventos. Los casos especiales se pueden manejar por separado del interruptor principal utilizando cualquier construcción que se considere necesaria.
Caso más complejo
Cuando el interruptor se hace más grande que un par de pantallas llenas, divídalo en funciones que manejan cada estado, usando una tabla de estado para buscar la función directamente. El estado aún es privado para el controlador de eventos. Las funciones del controlador de estado devuelven el siguiente estado. Si es necesario, algunos eventos aún pueden recibir un tratamiento especial en el controlador de eventos principal. Me gusta agregar pseudoeventos para la entrada y salida de estados y quizás el inicio de la máquina de estados:
No estoy seguro si clavé la sintaxis, especialmente con respecto a la matriz de punteros de función. No he ejecutado nada de esto a través de un compilador. Tras la revisión, noté que olvidé descartar explícitamente el siguiente estado al manejar los pseudoeventos (el paréntesis (nulo) antes de la llamada a state_handler ()). Esto es algo que me gusta hacer incluso si los compiladores aceptan la omisión en silencio. Le dice a los lectores del código que "sí, realmente quise llamar a la función sin usar el valor de retorno", y puede evitar que las herramientas de análisis estático avisen al respecto. Puede ser idiosincrásico porque no recuerdo haber visto a nadie más haciendo esto.
Puntos: agregar un poco de complejidad (verificar si el siguiente estado es diferente del actual), puede evitar el código duplicado en otro lugar, porque las funciones del controlador de estado pueden disfrutar de los pseudo eventos que ocurren cuando se ingresa y se deja un estado. Recuerde que el estado no puede cambiar cuando se manejan los pseudoeventos, porque el resultado del controlador de estado se descarta después de estos eventos. Por supuesto, puede optar por modificar el comportamiento.
Un controlador de estado se vería así:
Más complejidad
Cuando la unidad de compilación se vuelve demasiado grande (lo que sea que sientas, debería decir alrededor de 1000 líneas), coloca cada controlador de estado en un archivo separado. Cuando cada controlador de estado se alargue más que un par de pantallas, divida cada evento en una función separada, similar a la forma en que se dividió el interruptor de estado. Puede hacerlo de varias maneras, por separado del estado o mediante el uso de una tabla común o combinando varios esquemas. Algunos de ellos han sido cubiertos aquí por otros. Ordene sus tablas y use la búsqueda binaria si la velocidad es un requisito.
Programación genérica
Me gustaría que el preprocesador se ocupe de problemas como ordenar tablas o incluso generar máquinas de estado a partir de descripciones, lo que le permite "escribir programas sobre programas". Creo que esto es para lo que las personas de Boost están explotando las plantillas de C ++, pero encuentro que la sintaxis es críptica.
Tablas bidimensionales
He usado tablas de estado / evento en el pasado, pero tengo que decir que para los casos más simples no las considero necesarias y prefiero la claridad y la legibilidad de la declaración de cambio, incluso si se extiende más allá de una pantalla completa. Para casos más complejos, las tablas se descontrolan rápidamente como otros han señalado. Los modismos que presento aquí le permiten agregar una serie de eventos y estados cuando lo desee, sin tener que mantener una tabla que consuma memoria (incluso si puede ser memoria de programa).
Descargo de responsabilidad
Las necesidades especiales pueden hacer que estos modismos sean menos útiles, pero he descubierto que son muy claros y fáciles de mantener.
fuente
Extremadamente no probado, pero divertido de codificar, ahora en una versión más refinada que mi respuesta original; Las versiones actualizadas se pueden encontrar en mercurial.intuxication.org :
sm.h
ejemplo.c
fuente
Realmente me gustó la respuesta de paxdiable y decidí implementar todas las características faltantes para mi aplicación, como las variables de protección y los datos específicos de la máquina de estado.
Subí mi implementación a este sitio para compartir con la comunidad. Se ha probado con IAR Embedded Workbench para ARM.
https://sourceforge.net/projects/compactfsm/
fuente
Otra herramienta interesante de código abierto es Yakindu Statechart Tools en statecharts.org . Utiliza los gráficos de estado de Harel y, por lo tanto, proporciona estados jerárquicos y paralelos y genera código C y C ++ (así como Java). No utiliza bibliotecas, pero sigue un enfoque de "código simple". El código básicamente aplica estructuras de mayúsculas y minúsculas. Los generadores de código también se pueden personalizar. Además, la herramienta ofrece muchas otras características.
fuente
Llegando a esta hora (como siempre) pero escaneando las respuestas hasta la fecha, creo que falta algo importante;
He descubierto en mis propios proyectos que puede ser muy útil no tener una función para cada combinación válida de estado / evento. Me gusta la idea de tener efectivamente una tabla 2D de estados / eventos. Pero me gusta que los elementos de la tabla sean más que un simple puntero de función. En cambio, trato de organizar mi diseño para que, en esencia, comprenda un conjunto de elementos atómicos o acciones simples. De esa manera puedo enumerar esos elementos atómicos simples en cada intersección de mi tabla de estado / evento. La idea es que no tiene que definir una masa de N funciones cuadradas (generalmente muy simples). ¿Por qué tener algo tan propenso a errores, lento, difícil de escribir, difícil de leer, lo que sea?
También incluyo un nuevo estado opcional y un puntero de función opcional para cada celda de la tabla. El puntero de función está ahí para esos casos excepcionales en los que no desea simplemente disparar una lista de acciones atómicas.
Sabes que lo estás haciendo bien cuando puedes expresar muchas funciones diferentes, simplemente editando tu tabla, sin ningún código nuevo para escribir.
fuente
Sin embargo, creo que el mío es un poco diferente al de los demás. Un poco más de separación de código y datos de lo que veo en las otras respuestas. Realmente leí sobre la teoría para escribir esto, que implementa un lenguaje regular completo (sin expresiones regulares, por desgracia). Ullman, Minsky, Chomsky. No puedo decir que lo entendí todo, pero he sacado de los viejos maestros lo más directamente posible: a través de sus palabras.
Utilizo un puntero de función a un predicado que determina la transición a un estado 'sí' o un estado 'no'. Esto facilita la creación de un aceptador de estado finito para un lenguaje regular que usted programa de una manera más similar al lenguaje ensamblador. No se desanime por mis tontas opciones de nombre. 'czek' == 'verificar'. 'grok' == [ve a buscarlo en el Diccionario Hacker].
Entonces, para cada iteración, czek llama a una función de predicado con el carácter actual como argumento. Si el predicado devuelve verdadero, se consume el carácter (el puntero avanzado) y seguimos la transición 'y' para seleccionar el siguiente estado. Si el predicado devuelve falso, el carácter NO se consume y seguimos la transición 'n'. ¡Entonces cada instrucción es una rama bidireccional! Debo haber estado leyendo La historia de Mel en ese momento.
Este código viene directamente de mi intérprete PostScript y evolucionó a su forma actual con mucha guía de los compañeros en comp.lang.c. Dado que PostScript básicamente no tiene sintaxis (solo requiere corchetes equilibrados), un aceptador de lenguaje regular como este también funciona como analizador.
fuente
boost.org viene con 2 implementaciones diferentes de gráficos de estado:
Como siempre, el impulso te llevará al infierno de las plantillas.
La primera biblioteca es para máquinas de estado más críticas para el rendimiento. La segunda biblioteca le ofrece una ruta de transición directa de un diagrama de estado UML al código.
Aquí está la pregunta SO pidiendo una comparación entre los dos donde ambos autores responden.
fuente
Esta serie de publicaciones de Ars OpenForum sobre un poco complicado de lógica de control incluye una implementación muy fácil de seguir como una máquina de estado en C.
fuente
Vi esto en alguna parte
fuente
goto
eso crea una dependencia en un sistema operativo multitarea preventivo.Dado que usted implica que puede usar C ++ y, por lo tanto, el código OO, sugeriría evaluar el patrón de estado 'GoF' (GoF = Gang of Four, los chicos que escribieron el libro de patrones de diseño que trajo los patrones de diseño a la fama).
No es particularmente complejo y se usa y discute ampliamente, por lo que es fácil ver ejemplos y explicaciones en línea.
También es muy probable que sea reconocido por cualquier otra persona que mantenga su código en una fecha posterior.
Si la preocupación es la eficiencia, valdría realmente la evaluación comparativa para asegurarse de que un enfoque sin OO sea más eficiente, ya que muchos factores afectan el rendimiento y no siempre es simplemente un código funcional OO malo. Del mismo modo, si el uso de la memoria es una restricción para usted, nuevamente vale la pena hacer algunas pruebas o cálculos para ver si esto realmente será un problema para su aplicación en particular si usa el patrón de estado.
Los siguientes son algunos enlaces al patrón de estado 'Gof', como sugiere Craig:
fuente
Aquí hay un ejemplo de una máquina de estados finitos para Linux que usa colas de mensajes como eventos. Los eventos se ponen en la cola y se manejan en orden. El estado cambia según lo que ocurra para cada evento.
Este es un ejemplo para una conexión de datos con estados como:
Una pequeña característica adicional que agregué fue una marca de tiempo para cada mensaje / evento. El controlador de eventos ignorará los eventos que son demasiado antiguos (han caducado). Esto puede suceder mucho en el mundo real, donde podría quedar atrapado en un estado inesperado.
Este ejemplo se ejecuta en Linux, use el Makefile a continuación para compilarlo y jugar con él.
state_machine.c
Makefile
fuente
Su pregunta es bastante genérica.
Aquí hay dos artículos de referencia que pueden ser útiles,
Implementación de máquina de estado incorporada
fuente
He utilizado State Machine Compiler en proyectos Java y Python con éxito.
fuente
Esta es una publicación antigua con muchas respuestas, pero pensé que agregaría mi propio enfoque a la máquina de estados finitos en C. Hice un script de Python para producir el código C esqueleto para cualquier número de estados. Ese script está documentado en GituHub en FsmTemplateC
Este ejemplo se basa en otros enfoques sobre los que he leído. No usa las instrucciones goto o switch, sino que tiene funciones de transición en una matriz de puntero (tabla de consulta). El código se basa en una gran macro inicializadora multilínea y características C99 (inicializadores designados y literales compuestos), por lo que si no le gustan estas cosas, es posible que no le guste este enfoque.
Aquí hay un script de Python de un ejemplo de torniquete que genera código C esqueleto usando FsmTemplateC :
El encabezado de salida generado contiene los typedefs:
eFsmTurnstileCheck
se usa para determinar si una transición se bloqueóEFSM_TURNSTILE_TR_RETREAT
, si se permitió el progresoEFSM_TURNSTILE_TR_ADVANCE
o si la llamada a la función no fue precedida por una transición conEFSM_TURNSTILE_TR_CONTINUE
.eFsmTurnstileState
es simplemente la lista de estados.eFsmTurnstileInput
es simplemente la lista de entradas.FsmTurnstile
estructura es el corazón de la máquina de estados con la verificación de transición, la tabla de búsqueda de funciones, el estado actual, el estado ordenado y un alias a la función principal que ejecuta la máquina.FsmTurnstile
solo debe llamarse desde la estructura y debe tener su primera entrada como un puntero en sí mismo para mantener un estado persistente, estilo orientado a objetos.Ahora para las declaraciones de funciones en el encabezado:
Los nombres de las funciones están en el formato
{prefix}_{from}_{to}
, donde{from}
es el estado anterior (actual) y{to}
el siguiente. Tenga en cuenta que si la tabla de transición no permite ciertas transiciones, se establecerá un puntero NULO en lugar de un puntero de función. Finalmente, la magia ocurre con una macro. Aquí construimos la tabla de transición (matriz de enumeraciones de estado) y la tabla de búsqueda de funciones de transición de estado (una matriz de punteros de función):Al crear el FSM, la macro
FSM_EXAMPLE_CREATE()
se debe usar .Ahora, en el código fuente, todas las funciones de transición de estado declaradas anteriormente deben rellenarse. La
FsmTurnstileFopts
estructura se puede utilizar para pasar datos a / desde la máquina de estado. Cada transición debe establecersefsm->check
para que sea igual aEFSM_EXAMPLE_TR_RETREAT
bloquearla de la transición oEFSM_EXAMPLE_TR_ADVANCE
para permitirle hacer la transición al estado ordenado. Puede encontrar un ejemplo de trabajo en (FsmTemplateC) [ https://github.com/ChisholmKyle/FsmTemplateC] .Aquí está el uso real muy simple en su código:
Todo ese negocio de encabezado y todas esas funciones solo para tener una interfaz simple y rápida lo vale en mi mente.
fuente
Puede usar la biblioteca de código abierto OpenFST .
fuente
fuente
Yo personalmente uso estructuras de referencia automática en combinación con matrices de punteros. Subí un tutorial en github hace un tiempo, enlace:
https://github.com/mmelchger/polling_state_machine_c
Nota: Me doy cuenta de que este hilo es bastante antiguo, pero espero obtener opiniones y reflexiones sobre el diseño de la máquina de estado, así como poder proporcionar un ejemplo para un posible diseño de máquina de estado en C.
fuente
Se puede considerar UML-Estado-máquina-en-c , un marco máquina de estados "ligera" en C. He escrito este marco para apoyar tanto a máquina de estados finitos y máquina de estados jerárquicos . En comparación con las tablas de estado o casos de cambio simples, un enfoque de marco es más escalable. Se puede usar para máquinas de estado finito simples a máquinas de estado jerárquicas complejas.
La máquina de estados está representada por la
state_machine_t
estructura. Contiene solo dos miembros "Evento" y un puntero al "estado_t".state_machine_t
debe ser el primer miembro de su estructura de máquina de estado. p.ejstate_t
contiene un controlador para el estado y también controladores opcionales para la acción de entrada y salida.Si el marco está configurado para una máquina de estados jerárquica, entonces el
state_t
contiene un puntero al estado primario y secundario.Framework proporciona una API
dispatch_event
para enviar el evento a la máquina de estado yswitch_state
activar la transición de estado.Para obtener más detalles sobre cómo implementar una máquina de estados jerárquica, consulte el repositorio de GitHub .
ejemplos de código,
https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine/readme.md https://github.com/kiishor/UML-State-Machine-in-C /blob/master/demo/simple_state_machine_enhanced/readme.md
fuente
Aquí hay un método para una máquina de estados que usa macros de modo que cada función pueda tener su propio conjunto de estados: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code -a
Se titula "simular multitarea", pero ese no es el único uso.
Este método utiliza devoluciones de llamada para recoger en cada función donde se dejó. Cada función contiene una lista de estados únicos para cada función. Se usa un "bucle inactivo" central para ejecutar las máquinas de estado. El "bucle inactivo" no tiene idea de cómo funcionan las máquinas de estado, son las funciones individuales las que "saben qué hacer". Para escribir código para las funciones, uno simplemente crea una lista de estados y usa las macros para "suspender" y "reanudar". Usé estas macros en Cisco cuando escribí la Biblioteca Transceptor para el conmutador Nexus 7000.
fuente