Por que as corotinas estão de volta? [fechadas]

19

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?

user1787812
fonte
9
Não sei se eles foram embora.
Blrfl

Respostas:

26

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.

amon
fonte
Eu recomendo que você adquira o quarto ao último parágrafo para evitar o risco de plágio, é quase exatamente o mesmo que outra fonte que li. Além disso, embora tenha uma sobrecarga de ordens de magnitude menor que os threads, o desempenho das corotinas não pode ser simplificado para "uma chamada de função indireta". Consulte Aumenta os detalhes sobre as implementações de rotina aqui e aqui .
whn
1
@snb Em relação à sua edição sugerida: o GIL pode ser um detalhe de implementação do CPython, mas o problema fundamental é que a linguagem Python não possui um modelo de memória explícita que especifica mutação paralela de dados. O GIL é um truque para contornar esses problemas. Mas as implementações de Python com verdadeiro paralelismo devem se esforçar bastante para fornecer semânticas equivalentes, por exemplo, conforme discutido no livro Jython . Basicamente: todo campo de variável ou objeto deve ser uma variável volátil cara .
amon
3
@snb Com relação ao plágio: O plágio está falsamente apresentando idéias como suas, especialmente em um contexto acadêmico. É uma alegação séria , mas tenho certeza que você não quis dizer isso. O parágrafo "Threads são basicamente como processos" apenas reitera fatos conhecidos, como é ensinado em qualquer palestra ou livro de texto sobre sistemas operacionais. Como existem muitas maneiras de expressar concisamente esses fatos, não me surpreende que o parágrafo pareça familiar a você.
amon
Não mudei o significado para sugerir que o Python tinha um modelo de memória. Além disso, o uso de volátil não diminui por si só o desempenho volátil, simplesmente significa que o compilador não pode otimizar a variável de uma maneira que possa assumir que a variável permanecerá inalterada sem operações explícitas no contexto atual. No mundo Jython, isso pode realmente importar, pois ele usará a compilação VM JIT, mas no mundo CPython você não se preocupa com a otimização JIT, suas variáveis ​​voláteis existiriam no espaço de tempo de execução do interpretador, onde nenhuma otimização poderia ser feita. .
whn
7

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.

dlasalle
fonte
4
Mas as corotinas não são gerenciadas pelo sistema operacional. O sistema operacional não sabe o que uma co-rotina é, ao contrário de fibras C ++
overexchange
Muitos sistemas operacionais possuem corotinas.
Jörg W Mittag
correias como python e Javascript ES6 + não são multiprocessos? Como eles alcançam o paralelismo de tarefas?
whn
1
@Mael O recente "renascimento" de corotinas vem de python e javascript, os quais não alcançam paralelismo com suas corotinas, como eu entendo. Ou seja, essa resposta está incorreta, pois o paralelismo das tarefas não é a razão pela qual as corotinas estão "de volta". Luas também não são multiprocessos? Edição: Acabei de perceber que você não estava falando sobre paralelismo, mas por que você me respondeu em primeiro lugar? Responda a dlasalle, pois claramente eles estão errados sobre isso.
whn
3
@ dlasalle Não, eles não podem, apesar do fato de que diz "executando em paralelo" que não significa que nenhum código seja executado fisicamente ao mesmo tempo. O GIL o interromperia e o assíncrono não gera processos separados necessários para o multiprocessamento no CPython (GILs separados). O Async trabalha com rendimentos em um único thread. Quando eles dizem "parralel", na verdade eles significam várias funções relacionadas a outras funções de trabalho e execução de funções intercaladas . Os processos assíncronos do Python não podem ser executados em paralelo devido ao impl. Agora tenho três idiomas que não fazem corotinas parralel, Lua, Javascript e Python.
whn
5

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:

  1. eles consomem muitos recursos
  2. as alternâncias de contexto levam muito tempo, relativamente falando, e geralmente são desnecessárias
  3. eles destroem localidade de referência
  4. escrever código correto que coordena vários recursos que podem precisar de acesso exclusivo é inesperadamente difícil

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.

  1. As corotinas exigem pouco mais em termos de recursos do que um punhado de páginas para pilha, muito menos que a maioria das implementações de encadeamentos.
  2. As corotinas apenas alternam o contexto em pontos definidos pelo programador, o que, esperançosamente, significa apenas quando é necessário. Eles também geralmente não precisam preservar tanta informação de contexto (por exemplo, valores de registro) quanto os encadeamentos, o que significa que cada opção geralmente é mais rápida e precisa de menos delas.
  3. Padrões comuns de rotinas, incluindo operações do tipo produtor / consumidor, transferem dados entre rotinas de uma maneira que aumenta ativamente a localidade. Além disso, as alternâncias de contexto geralmente ocorrem apenas entre unidades de trabalho que não estão dentro delas, ou seja, no momento em que a localidade é geralmente minimizada de qualquer maneira.
  4. É menos provável que o bloqueio de recursos seja necessário quando as rotinas sabem que não podem ser arbitrariamente interrompidas no meio de uma operação, permitindo que implementações mais simples funcionem corretamente.
Jules
fonte
5

Prefácio

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 .

Uso moderno (por que eles estão de volta)

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.

Exemplo moderno

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 awaitdentro dele. Isso é basicamente um rendimento, que gera execução para o loopque permite executar uma função diferente (neste caso, uma factorialfunçã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 #

whn
fonte
@ T.Sar Eu nunca afirmei que o C # tinha nenhuma corotina "natural", nem o C ++ (pode mudar) nem o python (e ainda o tinha), e todos os três têm implementações co-rotineiras. Mas todas as implementações em C # de corotinas (como as da unidade) são baseadas no rendimento, como eu descrevo. Além disso, o uso de "hack" aqui não faz sentido, acho que todo programa é um hack, porque nem sempre foi definido no idioma. Não estou de maneira alguma misturando C # "sistema baseado em tarefas" com nada, nem sequer mencionei.
whn
Eu sugeriria tornar sua resposta um pouco mais clara. O C # tem o conceito de aguardar instruções e um sistema de paralelismo baseado em tarefas - usar C # e essas palavras, dando exemplos em python sobre como o python não é realmente verdadeiramente paralelo, pode causar muita confusão. Além disso, remova sua primeira frase - não é necessário atacar diretamente outros usuários em uma resposta como essa.
T. Sar - Restabelece Monica