A maioria das bases para as corotinas ocorreu nos anos 60/70 e depois parou em favor de alternativas (por exemplo, threads)
Existe alguma substância para o interesse renovado nas corotinas que vem ocorrendo em python e outras linguagens?
A maioria das bases para as corotinas ocorreu nos anos 60/70 e depois parou em favor de alternativas (por exemplo, threads)
Existe alguma substância para o interesse renovado nas corotinas que vem ocorrendo em python e outras linguagens?
Respostas:
Coroutines nunca saíram, foram ofuscadas por outras coisas nesse meio tempo. O interesse recentemente aumentado em programação assíncrona e, portanto, corotinas se deve em grande parte a três fatores: maior aceitação de técnicas funcionais de programação, conjuntos de ferramentas com pouco suporte para o verdadeiro paralelismo (JavaScript! Python!) E, o mais importante: as diferentes vantagens e desvantagens entre threads e corotinas. Para alguns casos de uso, as corotinas são objetivamente melhores.
Um dos maiores paradigmas de programação dos anos 80, 90 e hoje é OOP. Se olharmos para a história do POO e especificamente para o desenvolvimento da linguagem Simula, vemos que as classes evoluíram a partir de corotinas. O Simula foi projetado para simulação de sistemas com eventos discretos. Cada elemento do sistema era um processo separado que seria executado em resposta a eventos pela duração de uma etapa de simulação e, em seguida, renderia para permitir que outros processos fizessem seu trabalho. Durante o desenvolvimento do Simula 67, o conceito de classe foi introduzido. Agora, o estado persistente da corotina é armazenado nos membros do objeto e os eventos são acionados ao chamar um método. Para mais detalhes, considere a leitura do artigo O desenvolvimento das linguagens SIMULA por Nygaard & Dahl.
Então, em uma reviravolta engraçada que usamos corotinas o tempo todo, estávamos chamando de objetos e programação orientada a eventos.
Com relação ao paralelismo, existem dois tipos de linguagens: aquelas que possuem um modelo de memória adequado e as que não. Um modelo de memória discute coisas como “Se eu gravar em uma variável e depois ler essa variável em outro encadeamento, vejo o valor antigo ou o novo valor ou talvez um valor inválido? O que significa 'antes' e 'depois'? Quais operações são garantidas como atômicas? ”
Criar um bom modelo de memória é difícil, portanto esse esforço nunca foi feito para a maioria dessas linguagens dinâmicas de código aberto definidas e não definidas pela implementação: Perl, JavaScript, Python, Ruby, PHP. Obviamente, todas essas linguagens evoluíram muito além do "script" para o qual foram originalmente criadas. Bem, algumas dessas linguagens têm algum tipo de documento de modelo de memória, mas não são suficientes. Em vez disso, temos hacks:
O Perl pode ser compilado com suporte para threading, mas cada thread contém um clone separado do estado completo do intérprete, tornando os threads proibitivamente caros. Como único benefício, essa abordagem de compartilhamento de nada evita corridas de dados e força os programadores a se comunicarem apenas através de filas / sinais / IPC. O Perl não tem uma história forte para o processamento assíncrono.
O JavaScript sempre teve um rico suporte para programação funcional; portanto, os programadores codificariam manualmente continuações / retornos de chamada em seus programas onde precisassem de operações assíncronas. Por exemplo, com solicitações Ajax ou atrasos na animação. Como a Web é inerentemente assíncrona, existe muito código JavaScript assíncrono e o gerenciamento de todos esses retornos de chamada é imensamente doloroso. Portanto, vemos muitos esforços para organizar melhor essas chamadas de retorno (promessas) ou para eliminá-las completamente.
Python tem esse infeliz recurso chamado Global Interpreter Lock. Basicamente, o modelo de memória Python é “Todos os efeitos aparecem seqüencialmente porque não há paralelismo. Somente um thread executará o código Python por vez. ”Portanto, enquanto o Python possui threads, eles são tão poderosos quanto as corotinas. [1] Python pode codificar muitas corotinas através de funções de gerador com yield
. Se usado corretamente, isso por si só pode evitar a maior parte do inferno de retorno de chamada conhecido no JavaScript. O sistema async / waitit mais recente do Python 3.5 torna os idiomas assíncronos mais convenientes no Python e integra um loop de eventos.
[1]: Tecnicamente, essas restrições se aplicam apenas ao CPython, a implementação de referência do Python. Outras implementações como o Jython oferecem encadeamentos reais que podem ser executados em paralelo, mas precisam passar por um longo período para implementar um comportamento equivalente. Essencialmente: cada variável ou membro do objeto é uma variável volátil , de modo que todas as alterações são atômicas e são vistas imediatamente em todos os encadeamentos. Obviamente, o uso de variáveis voláteis é muito mais caro do que o uso de variáveis normais.
Eu não sei o suficiente sobre Ruby e PHP para assá-los corretamente.
Resumindo: algumas dessas linguagens têm decisões fundamentais de design que tornam o multithreading indesejável ou impossível, levando a um foco mais forte em alternativas como corotinas e em maneiras de tornar a programação assíncrona mais conveniente.
Finalmente, vamos falar sobre as diferenças entre corotinas e threads:
Threads são basicamente como processos, exceto que vários threads dentro de um processo compartilham um espaço de memória. Isso significa que os threads não são de maneira alguma "leves" em termos de memória. Os encadeamentos são agendados preventivamente pelo sistema operacional. Isso significa que as opções de tarefas têm uma alta sobrecarga e podem ocorrer em momentos inconvenientes. Essa sobrecarga possui dois componentes: o custo de suspender o estado do encadeamento e o custo de alternar entre o modo de usuário (para o encadeamento) e o modo do kernel (para o planejador).
Se um processo planejar seus próprios encadeamentos direta e cooperativamente, a alternância de contexto para o modo kernel é desnecessária e a alternância de tarefas é comparativamente cara para uma chamada de função indireta, como em: bastante barata. Esses fios leves podem ser chamados de fios verdes, fibras ou corotinas, dependendo de vários detalhes. Usuários notáveis de fios / fibras verdes foram as primeiras implementações de Java e, mais recentemente, as Goroutines em Golang. Uma vantagem conceitual das corotinas é que sua execução pode ser entendida em termos de fluxo de controle que passa explicitamente entre as corotinas. No entanto, essas corotinas não alcançam um paralelismo verdadeiro, a menos que sejam agendadas em vários segmentos do SO.
Onde são úteis as corotinas baratas? A maioria dos softwares não precisa de um zilhão de threads, portanto, os threads caros normais geralmente são bons. No entanto, a programação assíncrona às vezes pode simplificar seu código. Para ser usada livremente, essa abstração deve ser suficientemente barata.
E depois há a web. Como mencionado acima, a web é inerentemente assíncrona. As solicitações de rede simplesmente levam muito tempo. Muitos servidores da web mantêm um pool de threads cheio de threads de trabalho. No entanto, na maioria das vezes, esses encadeamentos ficam ociosos porque aguardam algum recurso, seja esperando um evento de E / S ao carregar um arquivo do disco, aguardando até que o cliente reconheça parte da resposta ou aguardando até um banco de dados consulta concluída. O NodeJS demonstrou fenomenalmente que um consequente design de servidor assíncrono e baseado em eventos funciona extremamente bem. Obviamente, o JavaScript está longe de ser a única linguagem usada para aplicativos da Web, então também há um grande incentivo para outras linguagens (notáveis em Python e C #) para facilitar a programação da Web assíncrona.
As corotinas costumavam ser úteis porque os sistemas operacionais não executavam agendamento preventivo . Depois que eles começaram a fornecer agendamento preventivo, era mais necessário abandonar o controle periodicamente em seu programa.
À medida que os processadores com vários núcleos se tornam mais prevalentes, as corotinas são usadas para obter paralelismo de tarefas e / ou manter alta a utilização de um sistema (quando um encadeamento de execução precisa aguardar um recurso, outro pode começar a ser executado em seu lugar).
O NodeJS é um caso especial, no qual as corotinas são usadas obtêm acesso paralelo ao IO. Ou seja, vários encadeamentos são usados para atender solicitações de E / S, mas um único encadeamento é usado para executar o código javascript. O objetivo de executar um código de usuário em um encadeamento de signle é evitar a necessidade de usar mutexes. Isso se enquadra na categoria de tentar manter alta a utilização do sistema, como mencionado acima.
Os primeiros sistemas usavam corotinas para fornecer simultaneidade principalmente porque são a maneira mais simples de fazê-lo. Os encadeamentos requerem uma quantidade razoável de suporte do sistema operacional (você pode implementá-los no nível do usuário, mas você precisará de alguma maneira de organizar o sistema para interromper periodicamente o seu processo) e é mais difícil de implementar, mesmo quando você tem o suporte .
Os threads começaram a assumir o controle mais tarde porque, nos anos 70 ou 80, todos os sistemas operacionais sérios os apoiavam (e, nos anos 90, até no Windows!), E são mais gerais. E eles são mais fáceis de usar. De repente, todos pensaram que os tópicos eram a próxima grande novidade.
No final dos anos 90, começaram a aparecer rachaduras e, no início dos anos 2000, tornou-se evidente que havia sérios problemas com os threads:
Com o tempo, o número de tarefas que os programas normalmente precisam executar a qualquer momento está crescendo rapidamente, aumentando os problemas causados por (1) e (2) acima. A disparidade entre a velocidade do processador e os tempos de acesso à memória tem aumentado, exacerbando o problema (3). E a complexidade dos programas em termos de quantos e quais tipos diferentes de recursos eles exigem tem aumentado, aumentando a relevância do problema (4).
Mas, ao perder um pouco de generalidade e colocar um ônus extra no programador para pensar em como seus processos podem operar juntos, as corotinas podem resolver todos esses problemas.
Quero começar afirmando uma razão pela qual as corotinas não estão recebendo ressurgimento, paralelismo. Em geral, as corotinas modernas não são um meio de alcançar o paralelismo baseado em tarefas, pois as implementações modernas não utilizam a funcionalidade de multiprocessamento. A coisa mais próxima que você chega disso são coisas como fibras .
As corotinas modernas surgiram como uma maneira de obter uma avaliação lenta , algo muito útil em linguagens funcionais como haskell, onde, em vez de iterar sobre um conjunto inteiro para executar uma operação, você poderia executar uma avaliação apenas da operação, conforme necessário ( útil para conjuntos infinitos de itens ou conjuntos grandes com terminação e subconjuntos antecipados).
Com o uso da palavra-chave Yield para criar geradores (que, por si só, atendem a parte das preguiçosas necessidades de avaliação) em linguagens como Python e C #, as coroutinas, na implementação moderna, não eram apenas possíveis, mas possíveis sem sintaxe especial na própria linguagem. (embora o python tenha adicionado alguns bits para ajudar). As co-rotinas ajudam na evasão preguiçosa com a idéia de futuros s, onde, se você não precisar do valor de uma variável naquele momento, poderá adiá-la até que solicite explicitamente esse valor (permitindo que você use o valor e avaliá-lo preguiçosamente em um momento diferente da instanciação).
Além da avaliação preguiçosa, porém, especialmente na esfera da web, essas rotinas ajudam a corrigir o inferno de retorno de chamada . As corotinas tornam-se úteis no acesso ao banco de dados, transações on-line, interface do usuário, etc., onde o tempo de processamento na máquina do cliente não resulta em acesso mais rápido ao que você precisa. O encadeamento pode cumprir a mesma coisa, mas requer muito mais sobrecarga nessa esfera e, em contraste com as corotinas, são realmente úteis para o paralelismo de tarefas .
Em resumo, à medida que o desenvolvimento da Web cresce e os paradigmas funcionais se fundem mais com as linguagens imperativas, as corotinas são uma solução para problemas assíncronos e avaliação preguiçosa. As corotinas chegam a espaços problemáticos em que a segmentação por multiprocessos e a segmentação em geral são desnecessárias, inconvenientes ou impossíveis.
Corotinas em linguagens como Javascript, Lua, C # e Python derivam suas implementações por funções individuais, cedendo o controle do thread principal a outras funções (nada a ver com chamadas do sistema operacional).
Em este exemplo python , temos uma função engraçado python com algo chamado await
dentro dele. Isso é basicamente um rendimento, que gera execução para o loop
que permite executar uma função diferente (neste caso, uma factorial
função diferente ). Observe que, quando diz "Execução paralela de tarefas" que é um nome impróprio, na verdade não está executando paralelamente, sua execução de função de intercalação por meio do uso da palavra-chave wait (que é um tipo especial de rendimento)
Eles permitem rendimentos de controle únicos e não paralelos para processos simultâneos que não são paralelos a tarefas , no sentido de que essas tarefas nunca operam ao mesmo tempo. Corotinas não são threads em implementações de linguagem moderna. Todas essas linguagens de implementação de co-rotinas são derivadas dessas chamadas de rendimento de função (que o programador precisa realmente inserir manualmente em suas co-rotinas).
EDIT: C ++ Boost coroutine2 funciona da mesma maneira, e sua explicação deve fornecer uma visão melhor do que estou falando com as pessoas, veja aqui . Como você pode ver, não há um "caso especial" com as implementações, coisas como fibras de reforço são uma exceção à regra e, mesmo assim, requerem sincronização explícita.
EDIT2: desde que alguém pensou que eu estava falando sobre o sistema baseado em tarefas c #, eu não estava. Eu estava falando sobre o sistema do Unity e implementações ingênuas de c #