Estou criando um pequeno projeto em C e C ++ misto. Estou construindo uma pequena máquina de estado no coração de um dos meus segmentos de trabalho.
Fiquei me perguntando se vocês, gurus do SO, compartilhariam suas técnicas de design de máquinas de estado.
OBSERVAÇÃO: Estou principalmente depois das técnicas de implementação testadas e experimentadas.
ATUALIZADO: Com base em todas as ótimas contribuições reunidas no SO, decidi sobre essa arquitetura:
c++
c
architecture
state-machine
jldupont
fonte
fonte
Respostas:
Máquinas de estado que eu projetei antes (C, não C ++) se resumem a uma
struct
matriz e um loop. A estrutura consiste basicamente em um estado e evento (para consulta) e uma função que retorna o novo estado, algo como:Em seguida, você define seus estados e eventos com definições simples (esses
ANY
são marcadores especiais, veja abaixo):Então você define todas as funções chamadas pelas transições:
Todas essas funções são escritas para não pegar variáveis e retornar o novo estado para a máquina de estados. Neste exemplo, variáveis globais são usadas para transmitir qualquer informação para as funções de estado, quando necessário.
O uso de globais não é tão ruim quanto parece, pois o FSM geralmente está trancado dentro de uma única unidade de compilação e todas as variáveis são estáticas nessa unidade (e é por isso que usei aspas em "global" acima - elas são mais compartilhadas no FSM, do que verdadeiramente global). Como em todos os globais, requer cuidados.
A matriz de transições define todas as transições possíveis e as funções que são chamadas para essas transições (incluindo a última catch-all):
O que isso significa é: se você estiver no
ST_INIT
estado e receber oEV_KEYPRESS
evento, ligue paraGotKey
.O funcionamento do FSM se torna um loop relativamente simples:
Como mencionado acima, observe o uso de
ST_ANY
curingas, permitindo que um evento chame uma função, independentemente do estado atual.EV_ANY
também funciona de maneira semelhante, permitindo que qualquer evento em um estado específico chame uma função.Também pode garantir que, se você chegar ao final da matriz de transições, receberá um erro informando que o FSM não foi construído corretamente (usando a
ST_ANY/EV_ANY
combinação.Eu usei código semelhante para isso em muitos projetos de comunicação, como uma implementação antecipada de pilhas e protocolos de comunicação para sistemas embarcados. A grande vantagem foi sua simplicidade e relativa facilidade na alteração da matriz de transições.
Não tenho dúvida de que haverá abstrações de nível superior que podem ser mais adequadas hoje em dia, mas suspeito que todas se resumem a esse mesmo tipo de estrutura.
E como
ldog
afirma um comentário, você pode evitar completamente os globais, passando um ponteiro de estrutura para todas as funções (e usando isso no loop de eventos). Isso permitirá que várias máquinas de estado funcionem lado a lado sem interferência.Basta criar um tipo de estrutura que mantenha os dados específicos da máquina (estado no mínimo) e use-os em vez dos globais.
A razão pela qual raramente fiz isso é simplesmente porque a maioria das máquinas de estado que escrevi são do tipo singleton (leitura única, durante o processo de inicialização do arquivo de configuração, por exemplo), não precisando executar mais de uma instância . Mas tem valor se você precisar executar mais de um.
fonte
int (*fn)(void*);
ondevoid*
está o ponteiro para os dados que cada função de estado recebe como parâmetro. Em seguida, as funções de estado podem usar os dados ou ignorá-los.As outras respostas são boas, mas uma implementação muito "leve" que usei quando a máquina de estado é muito simples se parece com:
Eu usaria isso quando a máquina de estados for simples o suficiente para que a abordagem de ponteiro de função e tabela de transição de estado seja um exagero. Isso geralmente é útil para análise caractere por caractere ou palavra por palavra.
fonte
Perdoe-me por violar todas as regras da ciência da computação, mas uma máquina de estado é um dos poucos (posso contar apenas duas vezes) onde uma
goto
declaração não é apenas mais eficiente, mas também torna seu código mais limpo e fácil de ler. Porquegoto
instruções são baseadas em rótulos, é possível nomear seus estados em vez de controlar uma confusão de números ou usar uma enumeração. Ele também cria um código muito mais limpo, já que você não precisa de toda a quantidade extra de ponteiros de função ou enormes instruções de chave e loops while. Eu mencionei que é mais eficiente também?Aqui está a aparência de uma máquina de estado:
Você entendeu a ideia geral. O ponto é que você pode implementar a máquina de estado de maneira eficiente e relativamente fácil de ler e gritar para o leitor que ele está olhando para uma máquina de estado. Observe que, se você estiver usando
goto
instruções, você ainda deve ter cuidado, pois é muito fácil dar um tiro no pé enquanto o faz.fonte
Você pode considerar o State Machine Compiler http://smc.sourceforge.net/
Este esplêndido utilitário de código-fonte aberto aceita uma descrição de uma máquina de estado em uma linguagem simples e a compila em qualquer uma das dezenas de outras línguas - incluindo C e C ++. O próprio utilitário é escrito em Java e pode ser incluído como parte de uma construção.
O motivo para fazer isso, em vez de codificar manualmente usando o padrão GoF State ou qualquer outra abordagem, é que, uma vez que sua máquina de estado é expressa como código, a estrutura subjacente tende a desaparecer sob o peso do clichê que precisa ser gerado para suportá-lo. O uso dessa abordagem oferece uma excelente separação de preocupações e você mantém a estrutura da sua máquina de estado 'visível'. O código gerado automaticamente entra nos módulos que você não precisa tocar, para que você possa voltar e mexer com a estrutura da máquina de estado sem afetar o código de suporte que você escreveu.
Desculpe, estou sendo muito entusiasmado e, sem dúvida, deixando todo mundo de lado. Mas é um utilitário de primeira linha e bem documentado também.
fonte
Verifique o trabalho de Miro Samek (blog State Space , site State Machines & Tools ), cujos artigos no C / C ++ Users Journal foram ótimos.
O site contém uma implementação completa (C / C ++) na licença de código aberto e comercial de uma estrutura de máquina de estado (QP Framework) , um manipulador de eventos (QEP) , uma ferramenta de modelagem básica (QM) e uma ferramenta de rastreamento (QSpy) que permite desenhar máquinas de estado, criar código e depurá-las.
O livro contém uma extensa explicação sobre o que / por que a implementação e como usá-lo e também é um ótimo material para entender os fundamentos das máquinas de estado hierárquico e finito.
O site também contém links para vários pacotes de suporte da placa para uso do software em plataformas embarcadas.
fonte
Fiz algo semelhante ao que o paxdiablo descreve. Somente em vez de uma matriz de transições de estado / evento, configurei uma matriz bidimensional de ponteiros de função, com o valor do evento como o índice de um eixo e o valor do estado atual como o outro. Então eu ligo
state = state_table[event][state](params)
e a coisa certa acontece. Células que representam combinações inválidas de estado / evento recebem um ponteiro para uma função que diz isso, é claro.Obviamente, isso só funciona se os valores de estado e evento forem intervalos contíguos e iniciarem em 0 ou fechar o suficiente.
fonte
#define STATE_LIST() \STATE_LIST_ENTRY(state1)\STATE_LIST_ENTRY(state2)\...
(uma nova linha implícita após cada\
) em que (re) define a macro de entrada quando usa a macro STATE_LIST. Exemplo - fazendo uma matriz de nomes de estado:#define STATE_LIST_ENTRY(s) #s , \n const char *state_names[] = { STATE_LIST() };\n #undef STATE_LIST_ENTRY
. Alguns trabalham para configurar primeiro, mas isso é extremamente poderoso. Adicionar novo estado -> garantido sem perdas.Um "framework" de máquina de estado C ++ baseado em modelo muito bom é dado por Stefan Heinzmann em seu artigo .
Como não há link para um download completo do código no artigo, tomei a liberdade de colar o código em um projeto e verificá-lo. O material abaixo é testado e inclui as poucas peças menores, mas bastante óbvias.
A principal inovação aqui é que o compilador está gerando código muito eficiente. As ações vazias de entrada / saída não têm custo. Ações de entrada / saída não vazias são incorporadas. O compilador também está verificando a integridade do gráfico de estados. Ações ausentes geram erros de vinculação. A única coisa que não é capturada é a falta
Top::init
.Essa é uma alternativa muito boa à implementação do Miro Samek, se você pode viver sem o que está faltando - isso está longe de ser uma implementação completa do UML Statechart, embora implemente corretamente a semântica da UML, enquanto o código do projeto de Samek não lida com saída / transição / ações de entrada na ordem correta.
Se esse código funcionar para o que você precisa fazer e você tiver um compilador C ++ decente para o seu sistema, provavelmente terá um desempenho melhor que a implementação C / C ++ do Miro. O compilador gera uma implementação de máquina de estado de transição O (1) achatada para você. Se a auditoria da saída da montagem confirmar que as otimizações funcionam como desejado, você se aproxima do desempenho teórico. Melhor parte: é um código relativamente pequeno e fácil de entender.
Código de teste a seguir.
fonte
A técnica que eu gosto para máquinas de estado (pelo menos para controle de programa) é usar ponteiros de função. Cada estado é representado por uma função diferente. A função pega um símbolo de entrada e retorna o ponteiro da função para o próximo estado. O monitor central de loop de despacho pega a próxima entrada, a alimenta no estado atual e processa o resultado.
A digitação fica um pouco estranha, já que C não tem como indicar os tipos de ponteiros de função retornando, então as funções de estado retornam
void*
. Mas você pode fazer algo assim:Então, suas funções de estado individuais podem ativar suas entradas para processar e retornar o valor apropriado.
fonte
Caso mais simples
Pontos: o estado é privado, não apenas para a unidade de compilação, mas também para o event_handler. Casos especiais podem ser tratados separadamente do comutador principal usando qualquer construção que seja necessária.
Caso mais complexo
Quando o comutador ficar maior que duas telas cheias, divida-o em funções que tratam de cada estado, usando uma tabela de estados para procurar diretamente a função. O estado ainda é privado para o manipulador de eventos. As funções do manipulador de estado retornam o próximo estado. Se necessário, alguns eventos ainda podem receber tratamento especial no manipulador de eventos principais. Eu gosto de lançar pseudo-eventos para entrada e saída de estado e talvez início de máquina de estado:
Não tenho certeza se preguei a sintaxe, especialmente em relação à matriz de ponteiros de função. Eu não executei nada disso através de um compilador. Ao revisar, notei que esqueci de descartar explicitamente o próximo estado ao manipular os pseudo-eventos (o parêntese (nulo) antes da chamada para state_handler ()). Isso é algo que eu gosto de fazer, mesmo que os compiladores aceitem a omissão silenciosamente. Diz aos leitores do código que "sim, eu realmente pretendi chamar a função sem usar o valor de retorno", e isso pode impedir as ferramentas de análise estática de alertá-lo. Pode ser idiossincrático, porque não me lembro de ter visto mais alguém fazendo isso.
Pontos: adicionar um pouquinho de complexidade (verificar se o próximo estado é diferente do atual), pode evitar códigos duplicados em outros lugares, porque as funções do manipulador de estados podem aproveitar os pseudo eventos que ocorrem quando um estado é inserido e deixado. Lembre-se de que o estado não pode mudar ao manipular os pseudo eventos, porque o resultado do manipulador de estados é descartado após esses eventos. Obviamente, você pode optar por modificar o comportamento.
Um manipulador de estado ficaria assim:
Mais complexidade
Quando a unidade de compilação se tornar muito grande (o que você acha que é, devo dizer em torno de 1000 linhas), coloque cada manipulador de estado em um arquivo separado. Quando cada manipulador de estados se tornar maior que duas telas, divida cada evento em uma função separada, semelhante à maneira como a troca de estado foi dividida. Você pode fazer isso de várias maneiras, separadamente do estado ou usando uma tabela comum ou combinando vários esquemas. Alguns deles foram abordados aqui por outros. Classifique suas tabelas e use a pesquisa binária se a velocidade for um requisito.
Programação genérica
Gostaria que o pré-processador lide com questões como classificar tabelas ou até gerar máquinas de estado a partir de descrições, permitindo que você "escreva programas sobre programas". Acredito que é para isso que as pessoas do Boost estão explorando modelos C ++, mas acho a sintaxe enigmática.
Mesas bidimensionais
Eu usei tabelas de estado / evento no passado, mas devo dizer que, nos casos mais simples, não as acho necessárias e prefiro a clareza e legibilidade da instrução switch, mesmo que ela ultrapasse uma tela inteira. Para casos mais complexos, as tabelas ficam fora de controle rapidamente, como outros observaram. Os idiomas que apresento aqui permitem que você adicione vários eventos e estados quando desejar, sem precisar manter uma tabela que consome memória (mesmo que seja memória de programa).
aviso Legal
Necessidades especiais podem tornar esses idiomas menos úteis, mas eu os achei muito claros e sustentáveis.
fonte
Extremamente não testado, mas divertido de codificar, agora em uma versão mais refinada do que minha resposta original; Versões atualizadas podem ser encontradas em mercurial.intuxication.org :
sm.h
example.c
fonte
Gostei muito da resposta do paxdiable e decidi implementar todos os recursos ausentes para o meu aplicativo, como variáveis de guarda e dados específicos da máquina de estado.
Carreguei minha implementação neste site para compartilhar com a comunidade. Foi testado usando o IAR Embedded Workbench for ARM.
https://sourceforge.net/projects/compactfsm/
fonte
Outra ferramenta interessante de código aberto é o Yakindu Statechart Tools em statecharts.org . Ele utiliza os gráficos de estados Harel e, portanto, fornece estados hierárquicos e paralelos e gera código C e C ++ (além de Java). Ele não faz uso de bibliotecas, mas segue uma abordagem de 'código simples'. O código basicamente aplica estruturas de casos de comutação. Os geradores de código também podem ser personalizados. Além disso, a ferramenta fornece muitos outros recursos.
fonte
Chegando tarde demais (como sempre), mas examinando as respostas até o momento, acho que falta algo importante;
Descobri em meus próprios projetos que pode ser muito útil não ter uma função para todas as combinações válidas de estado / evento. Eu gosto da idéia de efetivamente ter uma tabela 2D de estados / eventos. Mas eu gosto que os elementos da tabela sejam mais do que um simples ponteiro de função. Em vez disso, tento organizar meu design para que, no fundo, ele compreenda um monte de elementos ou ações atômicos simples. Dessa forma, posso listar esses elementos atômicos simples em cada interseção da minha tabela de estado / evento. A idéia é que você não precisa definir uma massa de N ao quadrado (normalmente muito simples) funções. Por que algo tão propenso a erros, demorado, difícil de escrever e de ler, o nome dele?
Eu também incluo um novo estado opcional e um ponteiro de função opcional para cada célula na tabela. O ponteiro de função existe para aqueles casos excepcionais em que você não deseja apenas disparar uma lista de ações atômicas.
Você sabe que está fazendo certo quando pode expressar muitas funcionalidades diferentes, apenas editando sua tabela, sem nenhum novo código para escrever.
fonte
Acho que o meu é um pouco diferente do de todos os outros. Um pouco mais de separação de código e dados do que vejo nas outras respostas. Eu realmente li a teoria para escrever isso, que implementa uma linguagem regular completa (sem expressões regulares, infelizmente). Ullman, Minsky, Chomsky. Não posso dizer que entendi tudo, mas me baseiei nos antigos mestres o mais diretamente possível: através de suas palavras.
Eu uso um ponteiro de função para um predicado que determina a transição para um estado 'yes' ou 'no'. Isso facilita a criação de um aceitador de estado finito para um idioma regular que você programa de maneira mais semelhante à linguagem assembly. Por favor, não se deixe levar por minhas escolhas de nome bobo. 'czek' == 'cheque'. 'grok' == [vá procurar no Dicionário Hacker].
Portanto, para cada iteração, o czek chama uma função predicada com o caractere atual como argumento. Se o predicado retornar verdadeiro, o caractere será consumido (o ponteiro avançado) e seguiremos a transição 'y' para selecionar o próximo estado. Se o predicado retornar falso, o caractere NÃO será consumido e seguiremos a transição 'n'. Portanto, toda instrução é um ramo de mão dupla! Eu devia estar lendo A história de Mel na época.
Esse código vem direto do meu intérprete postscript e evoluiu para sua forma atual com muita orientação dos colegas no comp.lang.c. Como o postscript basicamente não possui sintaxe (requer apenas colchetes balanceados), um Aceitador de linguagem regular como esse também funciona como analisador.
fonte
O boost.org vem com 2 implementações diferentes de gráfico de estado:
Como sempre, o impulso levará você ao inferno dos modelos.
A primeira biblioteca é para máquinas de estado mais críticas para o desempenho. A segunda biblioteca fornece um caminho de transição direto de um Statechart UML para o código.
Aqui está a pergunta SO solicitando uma comparação entre os dois, onde ambos os autores respondem.
fonte
Esta série de postagens do Ars OpenForum sobre um pouco complicado de lógica de controle inclui uma implementação muito fácil de seguir como uma máquina de estado em C.
fonte
Vi isso em algum lugar
fonte
goto
disso cria uma dependência de um sistema operacional multitarefa preventivo.Como você sugere que pode usar C ++ e, portanto, código OO, sugiro avaliar o padrão 'GoF'state (GoF = Gang of Four, os caras que escreveram o livro de padrões de design que trouxeram padrões de design para os holofotes).
Não é particularmente complexo e é amplamente utilizado e discutido, por isso é fácil ver exemplos e explicações on-line.
Também provavelmente será reconhecível por qualquer outra pessoa que mantenha seu código posteriormente.
Se a eficiência é a preocupação, vale a pena comparar o desempenho para garantir que uma abordagem não OO seja mais eficiente, pois muitos fatores afetam o desempenho e nem sempre é simplesmente OO ruim, código funcional bom. Da mesma forma, se o uso de memória é uma restrição para você, vale a pena fazer alguns testes ou cálculos para verificar se isso realmente será um problema para seu aplicativo em particular se você usar o padrão de estado.
A seguir, estão alguns links para o padrão de estado 'Gof', como Craig sugere:
fonte
Aqui está um exemplo de uma máquina de estado finito para Linux que usa filas de mensagens como os eventos. Os eventos são colocados na fila e tratados em ordem. O estado muda dependendo do que acontece para cada evento.
Este é um exemplo para uma conexão de dados com estados como:
Um pequeno recurso extra que adicionei foi um carimbo de data / hora para cada mensagem / evento. O manipulador de eventos ignorará os eventos que são muito antigos (eles expiraram). Isso pode acontecer muito no mundo real, onde você pode ficar preso em um estado inesperadamente.
Este exemplo é executado no Linux, use o Makefile abaixo para compilá-lo e brincar com ele.
state_machine.c
Makefile
fonte
Sua pergunta é bastante genérica.
Aqui estão dois artigos de referência que podem ser úteis,
Implementação de máquina de estado incorporado
fonte
Eu tenho usado o State Machine Compiler em projetos Java e Python com êxito.
fonte
Este é um post antigo com muitas respostas, mas pensei em adicionar minha própria abordagem à máquina de estados finitos em C. Fiz um script Python para produzir o código C do esqueleto para qualquer número de estados. Esse script está documentado no GituHub em FsmTemplateC
Este exemplo é baseado em outras abordagens sobre as quais eu li. Ele não usa instruções goto ou switch, mas possui funções de transição em uma matriz de ponteiro (tabela de consulta). O código baseia-se em uma grande macro inicializadora de várias linhas e recursos C99 (inicializadores designados e literais compostos). Portanto, se você não gosta dessas coisas, talvez não goste dessa abordagem.
Aqui está um script Python de um exemplo de catraca que gera código C de esqueleto usando FsmTemplateC :
O cabeçalho de saída gerado contém os typedefs:
eFsmTurnstileCheck
é usado para determinar se uma transição foi bloqueadaEFSM_TURNSTILE_TR_RETREAT
, permitida a progressãoEFSM_TURNSTILE_TR_ADVANCE
ou a chamada de função não foi precedida por uma transição comEFSM_TURNSTILE_TR_CONTINUE
.eFsmTurnstileState
é simplesmente a lista de estados.eFsmTurnstileInput
é simplesmente a lista de entradas.FsmTurnstile
estrutura é o coração da máquina de estados com a verificação de transição, a tabela de pesquisa de funções, o estado atual, o estado comandado e um alias para a função principal que executa a máquina.FsmTurnstile
só deve ser chamado a partir da estrutura e deve ter sua primeira entrada como ponteiro para si mesmo, a fim de manter um estado persistente, estilo orientado a objeto.Agora, para as declarações de função no cabeçalho:
Os nomes das funções estão no formato
{prefix}_{from}_{to}
, onde{from}
é o estado anterior (atual) e{to}
o próximo estado. Observe que, se a tabela de transição não permitir determinadas transições, um ponteiro NULL em vez de um ponteiro de função será definido. Finalmente, a mágica acontece com uma macro. Aqui construímos a tabela de transição (matriz de enumerações de estado) e as funções de transição de estado consultam a tabela (uma matriz de ponteiros de função):Ao criar o FSM, a macro
FSM_EXAMPLE_CREATE()
deve ser usada.Agora, no código-fonte, todas as funções de transição de estado declaradas acima devem ser preenchidas. A
FsmTurnstileFopts
estrutura pode ser usada para transmitir dados de / para a máquina de estado. Toda transição devefsm->check
ser igual a,EFSM_EXAMPLE_TR_RETREAT
para impedir a transição ouEFSM_EXAMPLE_TR_ADVANCE
permitir a transição para o estado comandado. Um exemplo de trabalho pode ser encontrado em (FsmTemplateC) [ https://github.com/ChisholmKyle/FsmTemplateC] .Aqui está o uso real muito simples em seu código:
Todo esse negócio de cabeçalho e todas essas funções apenas para ter uma interface simples e rápida vale a pena em minha mente.
fonte
Você pode usar a biblioteca de código aberto OpenFST .
fonte
fonte
Eu pessoalmente uso estruturas de auto-referência em combinação com matrizes de ponteiros. Carreguei um tutorial no github há algum tempo, link:
https://github.com/mmelchger/polling_state_machine_c
Nota: Percebo que esse encadeamento é bastante antigo, mas espero receber informações e idéias sobre o design da máquina de estado, além de poder fornecer um exemplo para um possível design de máquina de estado em C.
fonte
Você pode considerar UML-state-machine-in-c , uma estrutura de máquina de estado "leve" em C. Escrevi essa estrutura para dar suporte à máquina de estado finita e à máquina de estado hierárquica . Compare com tabelas de estado ou casos de comutação simples, uma abordagem de estrutura é mais escalável. Pode ser usado para máquinas de estados finitos simples para máquinas de estados hierárquicos complexos.
Máquina de estado é representada pela
state_machine_t
estrutura. Ele contém apenas dois membros "Evento" e um ponteiro para o "estado_t".state_machine_t
deve ser o primeiro membro da sua estrutura de máquina de estado. por exemplostate_t
contém um manipulador para o estado e também manipuladores opcionais para ação de entrada e saída.Se a estrutura estiver configurada para uma máquina de estado hierárquica, o
state_t
conterá um ponteiro para o estado pai e filho.O Framework fornece uma API
dispatch_event
para despachar o evento para a máquina de estado eswitch_state
disparar a transição de estado.Para mais detalhes sobre como implementar uma máquina de estado hierárquica, consulte o repositório GitHub .
exemplos 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
fonte
Aqui está um método para uma máquina de estados que usa macros, de modo que cada função possa ter seu próprio conjunto de estados: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code -at
É intitulado "simular multitarefa", mas esse não é o único uso.
Este método usa retornos de chamada para capturar em cada função de onde parou. Cada função contém uma lista de estados exclusivos para cada função. Um "loop inativo" central é usado para executar as máquinas de estado. O "loop inativo" não tem idéia de como funcionam as máquinas de estado; são as funções individuais que "sabem o que fazer". Para escrever código para as funções, basta criar uma lista de estados e usar as macros para "suspender" e "retomar". Usei essas macros na Cisco quando escrevi a Transceiver Library para o switch Nexus 7000.
fonte