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?
multithreading
finite-state-machine
Victor Sorokin
fonte
fonte
Respostas:
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.
[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.
fonte
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.
fonte
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.
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.
fonte
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:
fonte: Re: Interessante análise do encadeamento do kernel do linux pela IBM (arquivos da lista de discussão do linux-kernel)
fonte
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.
fonte
Threads são a única opção em dois casos:
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.
fonte
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:
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.
fonte
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.
fonte
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.
fonte
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.
fonte