Máquinas de estado vs threads

24

Alan Cox disse uma vez "Um computador é uma máquina de estado. Os threads são para pessoas que não podem programar máquinas de estado".
Como perguntar a Alan diretamente não é uma opção para mim, eu prefiro perguntar aqui: como é possível obter a funcionalidade multithread em linguagem de alto nível, como Java, usando apenas um thread e uma máquina de estado? Por exemplo, e se houver duas atividades a serem executadas (cálculos e E / S) e uma atividade puder ser bloqueada?
O uso de "somente máquina de estado" é uma alternativa viável ao multiencadeamento em idiomas de alto nível?

Victor Sorokin
fonte
4
Um computador também é uma máquina de Turing. No entanto, não é necessariamente útil programá-lo como uma máquina de Turing. A pilha em linguagens imperativas é uma coisa extremamente útil, e programas multithread permitem manter várias pilhas na memória ao mesmo tempo. Fazer o mesmo em uma máquina de estado é certamente possível, mas ainda mais confuso.
thiton
10
Alan era desenvolvedor de kernel do sistema operacional; este era o seu domian . Portanto, sua citação deve ser tomada nesse contexto. Ele estaria programando 'contra o metal', onde faz mais sentido usar esse modelo. Depois que o sistema operacional abstrai o hardware e suas propriedades intrínsecas (que "um computador é uma máquina de estado ..."), você tem a oportunidade e o benefício de poder usar outros modelos que fazem mais sentido em seu domínio . Quase todo jogo faz uso pesado de máquinas de estado.
Steven Evers
2
A segmentação é simplesmente um recurso do sistema operacional para gerenciar automaticamente alguns dos comutadores de máquinas de estado, se você desejar. Obviamente, você pode criar uma enorme máquina de estado que gerenciará tudo sozinha, mas isso é mais complicado. O mesmo pode ser dito sobre processos. Você pode dizer que os processos são para pessoas que também não podem programar máquinas de estado. Mas a abstração fornece uma interface muito mais simples e menos propensa a erros. Na minha opinião, isso é apenas mais uma "citação legal" que deve ser ouvida, contemplada e depois ignorada na realidade.
Yam Marcovic 23/09
2
"Mas a abstração [thread] fornece uma interface muito mais simples e menos propensa a erros." Isso parece ser falso. O número de pessoas que erraram na segurança da thread indica que ela parece causar erros.
S.Lott
1
Muitos comentários e respostas aqui interpretam a citação como anti-multitarefa em geral; Acredito que Alan Cox é apenas anti-threads e defenderia o uso de vários processos para atingir muitos dos objetivos pelos quais as pessoas usam threads. Tenha em mente que ele é um hacker Unix: garfo FTW. Não encontrei nenhum comentário dele diretamente na citação, mas aqui está um de Larry McVoy da lista de discussão do kernel do Linux que segue nessa direção: lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
Martin B

Respostas:

25

Tudo o que um encadeamento faz é intercalar operações para que partes do processo pareçam se sobrepor no tempo. Uma máquina de núcleo único com vários threads simplesmente salta: executa pequenos bits de código de um thread e depois muda para outro. Um planejador simples decide qual thread é a prioridade mais alta e é realmente executado no núcleo.

Em um computador de núcleo único, nada acontece "ao mesmo tempo". É tudo apenas execução intercalada.

Existem muitas, muitas maneiras de conseguir a intercalação. Muitos.

Digamos que você tenha um processo simples de dois threads que use um bloqueio simples para que os dois threads possam gravar em uma variável comum. Você tem seis blocos de código.

  • T1 antes do bloqueio
  • T1-com trava
  • T1 após bloqueio
  • T2-antes do bloqueio
  • T2-com trava
  • T2 após bloqueio

[Isso pode ocorrer em loop ou ter mais bloqueios ou o que for. Tudo o que faz é ficar mais tempo, não mais complexo.]

As etapas de T1 devem executar em ordem (T1-antes, T1-com, T1-depois) e as etapas de T2 devem executar em ordem (T2-antes, T2-com, T2-depois).

Além da restrição "em ordem", elas podem ser intercaladas de qualquer maneira. De qualquer forma. Eles podem ser executados conforme listado acima. Outra ordem válida é (T1 antes, T2 antes, bloqueio T2, bloqueio T1, T2 depois, T1 depois). Existem muitos pedidos válidos.

Esperar.

Esta é apenas uma máquina de estado com seis estados.

É um autômato de estado finito não determinístico. A ordem dos estados T1-xxx com estados T2-xxx é indeterminada e não importa. Portanto, há lugares em que o "próximo estado" é um sorteio.

Por exemplo, quando o FSM é iniciado, T1 antes ou T2 antes são os dois primeiros estados legítimos. Atirar uma moeda.

Digamos que surgiu T1-antes. Faça isso. Quando isso é feito, há uma escolha entre T1-com e T2-antes. Atirar uma moeda.

Em cada etapa do FSM, haverá duas opções (duas linhas - duas opções) e um sorteio pode determinar qual estado específico é seguido.

S.Lott
fonte
Obrigado, boa explicação. E quanto à máquina multi-core? Eu acho que não há maneira explícita de explorar núcleos na máquina de estado Java? É preciso confiar no sistema operacional para isso, certo?
Victor Sorokin
5
Uma máquina com vários núcleos torna o agendamento um pouco mais complexo. No entanto, todos os núcleos gravam em uma única memória comum; portanto, a Ordem de gravação na memória entre os dois núcleos significa que - essencialmente - estamos de volta à execução intercalada de gravações na memória. O sistema operacional explora núcleos e a JVM aproveita isso. Não há necessidade de pensar duas vezes sobre isso.
S.Lott
9

Escrever funções de bloqueio é para pessoas que não podem criar máquinas de estado;)

Threads são úteis se você não conseguir contornar o bloqueio. Nenhuma atividade fundamental do computador está realmente bloqueando, mas muitas delas são implementadas dessa maneira para facilitar o uso. Em vez de retornar um caractere ou "falha na leitura", uma função de leitura é bloqueada até que todo o buffer seja lido. Em vez de procurar a mensagem de retorno em uma fila e retornar se nenhuma for encontrada, uma função de conexão aguarda resposta.

Você não pode usar funções de bloqueio em uma máquina de estado (pelo menos uma que não pode "congelar").

E sim, usar máquina de estado é uma alternativa viável. Nos sistemas em tempo real, essa é a única opção, o sistema fornece uma estrutura para a máquina. O uso de threads e funções de bloqueio é apenas "a saída mais fácil", porque geralmente uma chamada para uma função de bloqueio substitui cerca de 3-4 estados na máquina de estados.

SF.
fonte
Uma falha na página de código em um programa de contexto único de execução é fundamentalmente realmente um bloqueio. Por definição, o código que possui um único contexto de execução não pode avançar adiante até que o próximo pedaço de código no fluxo esteja disponível.
David Schwartz
1
@ David Schwartz: verdade, é fundamentalmente um bloqueio; mas não é uma 'operação', pois não é algo que o código bloqueado faz, é algo que acontece com ele.
Javier
1
A leitura do arquivo não é fundamentalmente bloqueadora - ela sempre pode ser dividida em solicitar a leitura de um determinado local e adquirir os dados dos buffers depois que a solicitação foi atendida. E uma falha de página é uma solução alternativa para o uso de troca aleatória / heurística. Isso ocorrerá se um determinado estado tiver sido inserido antes que todos os dados necessários para sua execução sejam disponibilizados - falta de previsão, algo contra o conceito de máquina de estado. Se as operações de troca e troca fizerem parte da máquina de estado, não ocorrerá uma falha na página.
SF.
1
@ David Schwartz: A definição de comportamento de "bloqueio" está sempre sujeita a requisitos de "tempo real". Por exemplo: A falha na página de código é considerada sem bloqueio para aplicativos que precisam de capacidade de resposta na ordem de centenas de milissegundos. Por outro lado, se o aplicativo tiver requisitos rígidos em tempo real, ele não usaria memória virtual.
MaR
1
@Mar: ... ou use algoritmos de troca determinísticos que garantam a busca dos dados necessários antes que eles se tornem necessários.
SF.
9

Como se consegue a funcionalidade multithread em linguagem de alto nível, como Java, usando apenas um thread e máquina de estado? Por exemplo, e se houver duas atividades a serem executadas (cálculos e E / S) e uma atividade puder ser bloqueada?

O que você está descrevendo é chamado de multitarefa cooperativa , em que as tarefas recebem a CPU e espera-se que a abandone voluntariamente após uma quantidade determinada de tempo ou atividade. Uma tarefa que não coopera continuando a usar a CPU ou bloqueando as gengivas durante todo o trabalho e, além de ter um cronômetro de vigilância de hardware, não há nada que o código que supervisiona as tarefas possa fazer a respeito.

O que você vê nos sistemas modernos é chamado multitarefa preemptiva , que é onde as tarefas não precisam renunciar à CPU porque o supervisor faz isso por elas quando chega uma interrupção gerada por hardware. A rotina de serviço de interrupção no supervisor salva o estado da CPU e a restaura na próxima vez que a tarefa for considerada merecedora de um intervalo de tempo, depois restaura o estado de qualquer tarefa a ser executada em seguida e volta para ela como se nada tivesse acontecido . Essa ação é chamada de alternância de contexto e pode ser cara.

O uso de "somente máquina de estado" é uma alternativa viável ao multiencadeamento em idiomas de alto nível?

Viável? Certo. Sane? As vezes. O uso de threads ou de alguma forma de multitarefa cooperativa caseira (por exemplo, máquinas de estado) depende das compensações que você deseja fazer.

Os encadeamentos simplificam o design da tarefa até o ponto em que você pode tratar cada um como seu próprio programa que compartilha o espaço de dados com outras pessoas. Isso lhe dá a liberdade de se concentrar no trabalho em questão e não em todo o gerenciamento e serviço de limpeza necessário para fazer com que ele funcione uma iteração por vez. Mas como nenhuma boa ação fica impune, você paga por toda essa conveniência nas alternâncias de contexto. Ter muitos encadeamentos que produzem a CPU após realizar um trabalho mínimo (voluntariamente ou fazer algo que bloqueie, como E / S) pode consumir muito tempo do processador na alternância de contexto. Isso é especialmente verdadeiro se suas operações de bloqueio raramente bloqueiam por muito tempo.

Existem algumas situações em que a rota cooperativa faz mais sentido. Certa vez, tive que escrever algum software da terra do usuário para um pedaço de hardware que transmitisse muitos canais de dados através de uma interface mapeada na memória que exigia pesquisa. Cada canal era um objeto criado de forma que eu pudesse deixá-lo rodar como um encadeamento ou executar repetidamente um único ciclo de pesquisa.

O desempenho da versão multithread não foi bom, exatamente pelo motivo que descrevi acima: cada thread fazia um trabalho mínimo e depois produzia a CPU para que os outros canais pudessem ter algum tempo, causando muitas alternâncias de contexto. Deixar os encadeamentos livres até serem antecipados ajudou com a taxa de transferência, mas resultou em alguns canais não sendo atendidos antes que o hardware experimentasse uma saturação de buffer porque eles não obtiveram uma fatia de tempo tão cedo.

A versão single-threaded, que fazia as iterações de cada canal, corria como um macaco escaldado e a carga no sistema caía como uma rocha. A penalidade que paguei pelo desempenho adicional foi ter que fazer malabarismos com as tarefas. Nesse caso, o código para fazê-lo era simples o suficiente para que o custo de desenvolvimento e manutenção valesse a pena a melhoria de desempenho. Eu acho que essa é realmente a linha de fundo. Se meus tópicos estivessem esperando por alguma chamada do sistema, o exercício provavelmente não teria valido a pena.

Isso me leva ao comentário de Cox: threads não são exclusivamente para pessoas que não podem escrever máquinas de estado. Algumas pessoas são capazes de fazer isso, mas optam por usar uma máquina de estado enlatada (ou seja, um thread) no interesse de concluir o trabalho mais cedo ou com menos complexidade.

Blrfl
fonte
2

e se houver duas atividades a serem executadas (fazendo cálculos e fazendo E / S) e uma atividade puder bloquear?

Bem, honestamente, não consigo imaginar como lidar com o bloqueio de E / S sem threads. É chamado de bloqueio, afinal, apenas porque o código que o invoca precisa wait.

Pela minha leitura do e-mail original de Cox (abaixo), ele ressalta que a segmentação não é bem dimensionada. Quero dizer, e se houver 100 solicitações de E / S? 1000? 10000? Cox está apontando que ter um grande número de threads pode levar a problemas graves:

De: Alan Cox ([email protected])
Data: sexta-feira, 21 de janeiro de 2000 - 13:33:52 EST

o artigo da IBM), que se o seu aplicativo depender de um grande número de encadeamentos, você sempre vai se deparar com o planejador? muitas pessoas jogam muitos threads em um problema e pode realmente ser um design ruim.

Essa é a menor das suas preocupações. 1000 threads são 8Mb de pilhas do kernel e alternância de tarefas suficiente para garantir que você também desative a maioria do cache. Um computador é uma máquina de estado. Threads são para pessoas que não podem programar máquinas de estado.

Existem muitos casos em que o Linux definitivamente não está ajudando a situação, principalmente a E / S de blocos assíncronos.

Alan

fonte: Re: Interessante análise do encadeamento do kernel do linux pela IBM (arquivos da lista de discussão do linux-kernel)

mosquito
fonte
1
  • Em teoria, isso é verdade. Na vida real, os threads são apenas uma abstração eficiente usada para programar uma máquina de estado. Eles são tão eficientes que também podem ser usados ​​para programar Statecharts e redes de Petri (ou seja, comportamentos paralelos, em que as máquinas de estado são basicamente seqüenciais).

  • O problema com as máquinas de estado é a explosão combinatória. O número de estados de um computador com 4G RAM é de 2 ^ (2 ^ 32) estados (sem contar a unidade de disco 2T).

  • Para um homem cuja única ferramenta é um martelo, todo problema parece um prego.

mouviciel
fonte
1

Threads são a única opção em dois casos:

  • usar vários núcleos sem separação de memória.
  • para lidar com código externo que bloqueia.

O segundo é o motivo pelo qual a maioria das pessoas pensa que os threads são inevitáveis ​​para executar programação de E / S ou rede, mas isso geralmente ocorre porque eles não sabem que seu sistema operacional possui uma API mais avançada (ou não querem brigar com ele).

Quanto à facilidade de uso e legibilidade, sempre existem loops de eventos (como libev ou EventMachine ) que tornam a programação de uma máquina de estados quase tão simples quanto fazê-lo com threads, fornecendo controle suficiente para esquecer os problemas de sincronização.

mbq
fonte
1
O melhor dos dois mundos: pequenas máquinas de estado de bloqueio em threads criam códigos de aplicativos muito simples. E os segmentos se dividem muito bem em núcleos, se você os tiver. Se tudo não tiver pelo menos dois núcleos, em breve. telefones baseados ie quad núcleo ARM vindo de 2012.
Tim Williscroft
0

Uma boa maneira de entender como as máquinas de estado e o multithreading interagem é examinar os manipuladores de eventos da GUI. Muitos aplicativos / estruturas de GUI utilizam um único thread de GUI que pesquisará as possíveis fontes de entrada e chamará uma função para cada entrada recebida; essencialmente, isso poderia ser escrito como uma grande opção:

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

Agora, fica claro rapidamente que o nível de controle de alto nível nessa construção não pode ser alto: o manipulador do ButtonPressed deve terminar sem a interação do usuário e retornar ao loop principal, porque, caso contrário, não haverá mais eventos do usuário. pode ser processado. Se houver algum estado para salvar, esse estado deverá ser salvo em variáveis ​​globais ou estáticas, mas não na pilha; isto é, o fluxo de controle normal em uma linguagem imperativa é limitado. Você está essencialmente limitado a uma máquina de estado.

Isso pode ficar bastante confuso quando você tem sub-rotinas aninhadas que precisam salvar, por exemplo, um nível de recursão. Ou estão no meio da leitura de um arquivo, mas o arquivo está indisponível no momento. Ou são apenas em uma computação longa. Em todos esses casos, torna-se desejável salvar o estado da execução atual e retornar ao loop principal, e isso é multithreading . Nada mais nada menos.

A coisa toda ficou um pouco mais complicada com a introdução do multithreading preventivo (ou seja, o sistema operacional que decide quando os threads devem gerar controle), e é por isso que a conexão não está clara imediatamente hoje.

Portanto, para responder à pergunta final: Sim, a máquina de estado é uma alternativa, a maioria das GUIs funciona dessa maneira com o encadeamento da GUI. Apenas não force demais a máquina de estado, ela se torna insustentável rapidamente.

thiton
fonte
0

Perguntar se o uso de uma máquina de estado é viável em uma linguagem de alto nível é como perguntar se a escrita no assembler é uma alternativa viável ao uso de uma linguagem de alto nível. Ambos têm o seu lugar, dada a situação certa.

A abstração do uso do encadeamento facilita a implementação de sistemas paralelos mais complexos, mas, em última análise, todos os sistemas paralelos têm os mesmos problemas. Problemas clássicos como Deadlock / Livelock e inversão de prioridade são tão possíveis quanto em sistemas baseados em máquinas de estado, assim como em um sistema paralelo de memória compartilhada , NUMA ou mesmo CSP , se for suficientemente complexo.

Mark Booth
fonte
0

Eu não acho que é certo, as máquinas de estado são um conceito de computação muito 'elegante', mas, como você diz, são bastante complicadas. E coisas complicadas são difíceis de acertar. E as coisas que não estão certas são apenas quebradas; portanto, a menos que você seja um gênio da suposta estatura de Alan Cox, fique com coisas que você sabe que funcionam - deixe a 'codificação inteligente' para projetos de aprendizagem.

Você pode dizer quando alguém fez uma tentativa vã de fazer uma, pois (supondo que funcione bem) quando se trata de mantê-la, você acha que a tarefa é quase impossível. O 'gênio' original terá mudado deixando você com um monte de código pouco compreensível (como esse tipo de desenvolvedor não costuma deixar muitos comentários e muito menos documentação técnica).

Em alguns casos, uma máquina de estado será uma escolha melhor - estou pensando em coisas do tipo incorporado agora, onde alguns padrões de máquina de estado são usados ​​e usados ​​repetidamente e de uma maneira mais formalizada (ou seja, engenharia adequada :))

Também pode ser difícil acertar os threads, mas existem padrões para ajudá-lo - principalmente reduzindo a necessidade de compartilhar dados entre os threads.

O último ponto sobre isso é que os computadores modernos rodam em muitos núcleos de qualquer maneira, portanto, uma máquina de estado não tira realmente proveito dos recursos disponíveis. Rosquear pode fazer um trabalho melhor aqui.

gbjbaanb
fonte
2
Máquinas estatais não são nada complicadas! Máquinas de estado complexas são complicadas, mas todos os sistemas complexos também são: o)
MaR
2
-1 para "não tente". esse é o pior conselho que você pode dar.
Javier
1
-1 "Não tente"? Isso é tolice. Eu também contestaria sua afirmação de que as máquinas de estado são difíceis. Depois que você entra em algo como uma Máquina de Estado Fuzzy Heirarchal ... então sim, é um pouco mais complicado. Mas uma simples máquina de estado? Isso é algo bastante básico que todo segundo ano aprendia quando eu estava na escola.
Steven Evers
deixe-me reformular o bit 'dont try' ... #
24711 gbjbaanb
0

Bom exemplo de uso adequado da máquina de estado em vez de threads: nginx vs apache2. Geralmente, você pode assumir que o nginx lida com todas as conexões em um thread, o apache2 faz um thread por conexão.

Mas, para mim, usar máquinas de estado x threads é bastante semelhante ao usar asm vs java perfeitamente criados à mão: você pode obter resultados inacreditáveis, mas são necessários muitos esforços dos programadores, muita disciplina, tornar o projeto mais complexo e valer apenas quando usado por muitos outros programadores. Portanto, se você é quem quer criar um servidor da Web rápido - use máquinas de estado e E / S assíncronas. Se você estiver escrevendo o projeto (não a biblioteca a ser usada em qualquer lugar) - use threads.

ZeusTheTrueGod
fonte