- Em Go, há padrões de concorrência que ficam estranhos usando apenas goroutines e canais, e corrotinas são uma proposta para ajudar a estruturar programas passando explicitamente o fluxo de execução, sem paralelismo
- Corrotinas trocam o controle de execução com
resume e yield; como apenas uma roda por vez, evitam corridas em dados compartilhados, e os pontos de troca se tornam pontos de sincronização
- Generators do Python e iterators do CLU se parecem com corrotinas, mas a posição de
yield é limitada; por isso, ao transpor diretamente uma travessia de árvore aninhada no estilo Lua, alguns valores desaparecem
- O
coro.New do Go também pode ser expresso com canais e goroutines, e coro.Pull transforma um push iterator em um pull iterator que extrai um valor a cada chamada
- Uma implementação baseada em canais levava cerca de 190 ns por troca, enquanto a troca direta no runtime reduziu isso para cerca de 20 ns por troca e cerca de 40 ns por valor em
coro.Pull, com o objetivo de evitar gargalos no uso real
Modelo de execução das corrotinas
- Corrotinas parecem chamadas de função, mas rodam em stacks diferentes e não executam ao mesmo tempo
- Mesmo que
F inicie G, G não roda imediatamente; só executa quando F faz resume explicitamente
- Durante a execução,
G pode devolver o controle de execução a F a qualquer momento com yield
- Quando
G retorna, ela é finalizada, e F recebe um sinal de que não deve mais fazer resume em G
- Nesse modelo, apenas uma corrotina por vez executa, enquanto o chamador espera em outro stack
- Como as trocas de execução só acontecem em pontos específicos do programa, vários fluxos executam alternadamente de forma coordenada
Corrotinas vistas por um exemplo em Lua
- O exemplo em Lua 5 compara se duas árvores binárias com estruturas diferentes têm a mesma sequência de valores
t1 e t2 contêm 1, 2, 3, 4, 5
t3 contém 1, 2, 3, 4, 6
visit(t) percorre a árvore em ordem e emite cada valor com coroutine.yield(t.value)
- A função de comparação cria duas corrotinas de
visit e lê o próximo valor alternando chamadas a coroutine.resume
- Se o estado de término ou o valor das duas corrotinas for diferente, retorna
false
- Se ambas terminarem, retorna
true
- Um código Lua mais idiomático usa
coroutine.wrap para obter uma função next que esconde o objeto de corrotina
- Quando a corrotina termina, a função
next retorna nil
- O código completo está no Gist
Limitações dos generators do Python e dos iterators do CLU
- Generators do Python parecem corrotinas de Lua, mas não seguem o mesmo modelo
- Se o exemplo em Lua for traduzido diretamente para Python,
visit(t['left']) não executa a travessia de fato; apenas cria um objeto generator e o descarta
- Se há
yield no corpo da função, def visit não define uma função comum, e sim um generator
- O exemplo traduzido de forma simples imprime apenas 4 da árvore e perde 1, 2, 3 e 5
- O código Python correto precisa iterar explicitamente sobre os generators aninhados e fazer
yield novamente
- O
yield from do Python 3.3 simplifica esse padrão
- Um objeto generator do Python contém apenas o estado de uma única chamada a
visit
- Os valores das variáveis locais e a linha em execução ficam armazenados no objeto generator
- Quando retomado, esse estado sobe para a pilha de chamadas e, no
yield, sai de volta para o objeto generator
yield só pode ocorrer no frame de chamada mais externo
- O CLU chamava essa abstração de iterator e distinguia estaticamente
iter de proc
- Graças às informações de tipo, o compilador conseguia diagnosticar usos em que um iterator era chamado por engano como uma função comum
- O artigo de 1977 de Barbara Liskov e outros, “Abstraction Mechanisms in CLU”, explica que o iterator é uma forma restrita de corrotina implementada apenas com o stack do programa
Diferenças entre corrotinas, threads e generators
- Os três conceitos oferecem alguma forma de concorrência, mas diferem em poder e custo
-
Corrotinas
- Oferecem concorrência sem paralelismo
- Enquanto uma corrotina está executando, a corrotina que a retomou ou para a qual ela cedeu o controle não executa
- Como os pontos de troca são explícitos, não surgem corridas ao compartilhar dados
- Trocas como chamadas a
coroutine.resume ou next viram pontos de sincronização e criam uma aresta happens-before
- Como o escalonamento é explícito, sem o sistema operacional, as trocas podem chegar a aproximadamente 10 ns ou menos
-
Threads
- São mais poderosas que corrotinas, e esse poder adicional é o paralelismo
- O custo disso é overhead de escalonamento, trocas de contexto mais caras e a necessidade de alguma forma de preempção
- Uma troca típica de thread fica na casa de alguns microssegundos
-
Goroutines do Go
- Nesta classificação, são mais próximas de threads baratas
- O runtime do Go assume parte do escalonamento, e as trocas ficam mais perto de algumas centenas de ns
- Como threads, oferecem paralelismo e preempção
- As novas lightweight threads do Java são basicamente iguais às goroutines
-
Generators
Casos em que corrotinas são necessárias em Go
- As bibliotecas de concorrência existentes em Go não oferecem diretamente o padrão de corrotinas
- Goroutines muitas vezes são parecidas o suficiente, mas por causa do paralelismo e da preempção podem produzir resultados diferentes dos de corrotinas
- A apresentação de 2011 de Rob Pike, “Lexical Scanning in Go”, trata do projeto inicial do lexer e do parser do pacote
text/template
- O lexer e o parser eram executados em goroutines separadas e conectados por canais
- Essa era uma estrutura que imitava de forma imperfeita um par de corrotinas
- O lexer olhava antecipadamente o próximo token enquanto o parser processava o token mais recente
- Generators não eram suficientes para um lexer que precisava fazer
yield de valores a partir de várias funções
- O paralelismo das goroutines criou corridas, e o projeto acabou mudando para armazenar o estado do lexer em um objeto
- Se houvesse corrotinas adequadas, elas teriam evitado as corridas e sido mais eficientes que goroutines
- Um caso de uso futuro é a iteração por coleções genéricas
- Em Go, já houve discussões sobre suporte a range sobre funções
- Isso poderia incentivar autores de coleções e abstrações a fornecer funções iterator no estilo CLU
- Em Go, hoje já é possível implementar push iterators usando valores de função
- Exemplo:
func (t *Tree[V]) All(yield func(v V))
- Atualmente, é possível chamar como
t.All(func(v V) { fmt.Println(v) })
- No futuro, uma forma como
for v := range t.All poderia ser possível
- O problema são travessias que não cabem em um único loop
for
- Há casos, como a comparação de árvores binárias, em que duas travessias precisam ser intercaladas
- Corrotinas podem transformar um push iterator como
(*Tree).All em um pull iterator que retorna um valor por chamada
coro.New expresso em Go puro
- Se corrotinas fossem adicionadas ao Go, isso deveria ser possível sem mudanças na linguagem, e elas deveriam poder ser entendidas e implementadas como código Go comum
- Um
coro.New simples é expresso com channels e goroutines
cin entrega valores de entrada
cout devolve valores de saída
resume envia um valor para cin e espera o resultado em cout
- A nova goroutine fica inicialmente bloqueada em
<-cin, portanto não há oportunidade de execução paralela
- Ao adicionar
yield, f pode emitir um valor durante a execução, e o chamador pode inserir novamente um valor no próximo resume
yield(out) envia um valor para cout e espera a próxima entrada em cin
- Isso também é um par send-receive, portanto não há paralelismo
- Esse padrão de comunicação restringe a goroutine a se comportar como uma corrotina
- Na prática, ainda é uma goroutine, mas
resume e yield atuam como operações de troca de contexto
Exemplo de parser de strings
- O problema em “Storing Data in Control Flow” é executar
func parseQuoted(read func() byte) bool em um fluxo de controle separado e fornecer bytes um por um por meio do método Write
- Usando
coro.New, é possível escrever em um nível mais alto do que a implementação temporária baseada em channels do artigo anterior
Init define a função coparse
read faz yield de NeedMoreInput e depois retorna o byte enviado pelo chamador
- O resultado booleano de
parseQuoted(read) é convertido em BadInput ou Success
p.resume(0) avança até o primeiro read de parseQuoted
Write(c byte) se torna um wrapper fino que chama p.resume(c)
- O código completo está no Go Playground
Exemplo do crivo de números primos
- O crivo concorrente de números primos, de Doug McIlroy, é um pipeline com uma corrotina para cada primo
p
- Cada filtro recebe números do vizinho à esquerda e, se não forem divisíveis por
p, os repassa ao vizinho à direita
- O counter na extremidade esquerda fornece 2, 3, 4, ...
- A corrotina de saída na extremidade direita lê e imprime os primos e cria uma nova corrotina de filtro
counter é uma função que envolve com coro.New um loop que faz yield dos valores
more bool informa se deve continuar gerando
yield(i) emite o valor e recebe a próxima indicação de continuidade
filter(p, next) obtém valores de next(true), a corrotina à esquerda, e só faz yield(n) quando n%p != 0
main mantém em next a saída atual do pipeline
- Lê o primo
p
- Imprime
p
- Adiciona à direita do pipeline um novo filtro que remove os múltiplos de
p
- A relação de chamadas entre as corrotinas pode mudar durante a execução
- O primeiro
yield do counter vai para main, mas os yields seguintes vão para o filtro de 2
- A primeira saída de cada filtro de
p vai para main como o próximo primo, e as saídas seguintes vão para o próximo filtro
- O código completo está no Go Playground
Relação entre goroutines e corrotinas
- O fluxo de controle criado aqui é, estritamente falando, uma goroutine
- Ele pode fazer tudo que uma goroutine comum pode fazer, como esperar em mutexes, channels e system calls
coro.New cria uma goroutine que pode usar operações de troca de corrotina dentro de yield e resume
- A instrução
go cria um novo fluxo de controle concorrente e paralelo, mas coro.New cria um novo fluxo de controle concorrente e não paralelo
- Se você executar 10 instruções
go, 11 goroutines, incluindo main, poderão ser executadas simultaneamente
- Se você chamar
coro.New 10 vezes, haverá 11 fluxos de controle, mas o paralelismo do programa permanecerá o mesmo, com apenas um executando por vez
- Qual goroutine desempenha o papel de corrotina “não paralela” pode mudar durante a execução
- Isso é igual ao fato de que qual goroutine está enviando ou recebendo em um channel pode mudar durante a execução
Um resume mais robusto
- O
coro.New inicial causa deadlock se resume for chamado depois que a função termina
- Para corrigir isso,
resume retorna um bool junto com o resultado
true significa que o resultado veio de yield
- Quando a função retorna,
resume retorna o valor de retorno e false
- Se
resume for chamado depois que a corrotina terminou, ele retorna o zero value e false
- A variável
running acompanha se f está em execução
- Como
resume e a corrotina executam alternadamente, o compartilhamento de running não é uma race
- O exemplo imprime
"hello" true, "world" true, "done" false, "" false
Conversão de iterator com coro.Pull
coro.Pull transforma um push iterator em um pull iterator
- O formato do push iterator de entrada é o seguinte
push func(yield func(V) bool)
- O retorno booleano de
yield indica se deve continuar
- O formato do pull iterator desejado é o seguinte
pull func() (V, bool)
- Ele retorna um valor e se a iteração terminou, como um receive de channel ou uma busca em map
- Para interrupção antecipada,
Pull retorna não apenas pull, mas também stop
- Para implementar, basta criar um pequeno wrapper que executa o push iterator com
coro.New
pull chama resume(true)
stop chama resume(false)
- O método
All da árvore é alterado para usar o resultado bool de yield
- Ele encadeia a travessia à esquerda, o yield do valor atual e a travessia à direita com
&&, propagando a interrupção antecipada
- A função de comparação de árvores cria dois
coro.Pull e compara os valores um a um
- Usa
defer stop1() e defer stop2() para parar as corrotinas em caso de término antecipado
- Se o valor ou o estado de término forem diferentes, retorna
false
- Se ambas terminarem, retorna
true
- O código completo está no Go Playground
Propagação de panic e cancelamento
- Um panic ocorrido em uma corrotina pode ser devolvido ao chamador que mais recentemente deu
resume nessa corrotina
- Em uma goroutine comum, é difícil saber para qual goroutine avisar e se essa goroutine está pronta para receber o aviso
- Em uma corrotina, o chamador está bloqueado esperando em
resume, então o destino para entregar o panic é claro
- A implementação transmite uma mensagem contendo um valor ou um panic por
cout
- O
defer da nova corrotina captura o panic
- O
resume que está esperando dá panic novamente com o mesmo valor de panic
- No exemplo, a corrotina faz yield de
"hello" e depois dá panic com "world"
- O panic é propagado para a goroutine main e, na stack, parece ter ocorrido na chamada a
resume
- O código completo está no Go Playground
- Para avisar a corrotina quando o chamador encerra antes do tempo, foi adicionada uma função
cancel
cancel é parecida com resume, mas faz com que yield dê panic em vez de retornar um valor
- O panic de cancelamento usa um wrapper de erro exclusivo que satisfaz
ErrCanceled
- Um panic causado por
cancel não é propagado de volta, mas, se a corrotina produzir outro panic durante o cancelamento, ele é propagado
- Se
resume ainda não tiver sido chamado, cancel impede que f seja executada
- Para interromper um iterator, um
bool explícito é mais claro do que panic, portanto Pull mantém a interrupção baseada em bool
Revisitando a peneira de primos: limpeza e propagação de erros
- Na nova API,
counter e filter retornam juntos a função resume e a função cancel
primes(n int) cria um counter e registra defer cancel()
- Lê e imprime cada número primo
- Sempre que adiciona um novo filter, também registra o
cancel desse filter com defer
- Quando a função obtém
n primos e retorna, as chamadas cancel adiadas limpam as corrotinas criadas
- Se alguma corrotina der panic, ele é propagado para a corrotina que estava esperando
- Se for uma corrotina retomada diretamente por
primes com next, o panic volta para primes
- Se for uma corrotina retomada por um filter com
next, o panic sobe pela cadeia de filters até p := next(true) em primes
- Depois disso, os
cancel adiados de primes limpam as corrotinas restantes
- O código completo está no Go Playground
Forma final da API
New cria uma nova corrotina suspensa e prepara a execução da função f
- A nova corrotina é uma goroutine, mas não executa por conta própria
- Ela executa apenas enquanto outra goroutine chama
resume ou cancel e espera
resume(in) pausa a goroutine chamadora e alterna para a nova corrotina
- A primeira chamada inicia
f(in, yield)
resume fica bloqueado até que f chame yield(out) ou retorne out
- Quando
yield é chamado, resume retorna out, true
- Quando
f retorna, resume retorna out, false
- O próximo
resume(in) faz com que o yield que estava bloqueado retorne in
cancel interrompe a execução de f e encerra a corrotina
- Se
resume nunca tiver sido chamado, f não é executada
- Caso contrário, o
yield que estava bloqueado dá panic com um erro que satisfaz ErrCanceled
- Se
f gerar um panic que não conseguir recuperar, esse panic se move para a goroutine que está esperando em resume ou cancel e dá panic novamente com o mesmo valor
- Porém,
cancel não dá panic novamente com o panic de cancelamento que ele próprio causou
- Se
f retornar ou der panic, a corrotina deixa de existir
- Chamadas posteriores a
resume retornam o zero value e false
- Chamadas posteriores a
cancel simplesmente retornam
resume, cancel e yield podem ser passados para outra goroutine e usados lá
- Como resultado, qual goroutine é a “corrotina” pode mudar dinamicamente
New cria uma nova goroutine, mas mantém a invariante de que sempre há uma goroutine bloqueada em resume, cancel, yield ou no estado inicial de espera
- Essa invariante é mantida até que
f retorne
- Como resultado,
coro.New cria nova concorrência, mas não cria novo paralelismo
- A assinatura final é a seguinte
func New[In, Out any](f func(in In, yield func(Out) In) Out) (resume func(In) (Out, bool), cancel func())
Eficiência
- Deve ser possível definir corrotinas com uma implementação em Go puro, mas o uso real exige uma implementação otimizada no runtime
- Em um MacBook Pro de 2019, o
coro.New baseado em canais leva cerca de 190ns por troca em uma ida e volta de valor
- Em
coro.Pull, isso equivale a cerca de 380ns por valor
coro.Pull não é a forma padrão de usar um iterator
- A forma padrão é chamar o iterator diretamente; nesse caso, não há overhead de corrotina
coro.Pull é necessário quando os valores precisam ser processados incrementalmente, e não em um único loop for
- A primeira tentativa de otimização foi fazer o compilador marcar pares send-receive e deixar uma dica para que o runtime os combinasse em uma única operação
- O runtime de canais pode contornar o scheduler e saltar diretamente para outra corrotina
- Leva cerca de 118ns por troca e cerca de 236ns por valor pulled
- É 38% mais rápido que a implementação original com canais
- A segunda implementação evita completamente canais e adiciona a troca de corrotinas diretamente ao runtime
- A troca de corrotinas é reduzida a 3 operações atomic compare-and-swap
- Uma é usada para a estrutura de dados da corrotina, uma para o status no scheduler da corrotina que será bloqueada e uma para o status no scheduler da corrotina que será retomada
- Leva cerca de 20ns por troca e cerca de 40ns por valor pulled
- É cerca de 10 vezes mais rápida que a implementação original com canais
- 40ns por valor é considerado um custo absoluto pequeno o bastante para não se tornar gargalo em código que precisa de
coro.Pull
1 comentários
Opiniões no Hacker News
Parece que muita gente aqui está perdendo o ponto central. É verdade que uma biblioteca de corrotinas é uma forma pior e mais trabalhosa de lidar com concorrência do que a palavra-chave
goO caso de uso real que traz essa complexidade são os iteradores de função, permitindo usar
rangeem funções do tipofunc() (T, bool). Isso já foi discutido por muito tempo na comunidade Go, e o significado deve ser intuitivo para a maioria dos programadores GoO texto trata do problema seguinte: se iteradores de função forem adicionados à linguagem, como escrever iteradores para usar em loops
for. Ele começa pelo ponto de que iteradores push muitas vezes são fáceis de escrever, expande para um adaptador push-pull, e esse adaptador é construído sobre corrotinasSe tudo isso entrar, acho que usar corrotinas para algo que não seja iteração será uma má prática, como usar canais/goroutines onde um mutex seria suficiente
Quando duas tarefas cooperam logicamente de forma síncrona, por exemplo no caso de um iterador, é muito mais eficiente executar tudo na mesma CPU. O kernel não precisa reescalonar nada nem desligar e acordar núcleos de CPU, e os dados continuam ajustados ao cache da CPU, melhorando a latência de cache e a taxa de acerto
Com goroutines isso pode acontecer por acaso, mas não é garantido, e no mínimo há o custo de passar pelo escalonador de goroutines do runtime do Go. Ele é rápido, mas não tão rápido quanto executar outro contexto de código dentro da mesma goroutine
Corrotinas permitem saber que a tarefa A alterna diretamente para a tarefa B, então o comportamento de escalonamento é mais previsível. No fim do texto, Russ mostra que uma implementação otimizada de corrotinas no runtime é 10 vezes mais rápida do que uma emulada com goroutines
O Google tem internamente um patch de kernel que implementa esse tipo de multithreading cooperativo, chamado internamente de fibers. Ele existe para obter melhor latência e escalonamento previsível. Paul Turner também fez uma apresentação no LPC, cerca de 10 anos atrás, explicando essa motivação: https://www.youtube.com/watch?v=KXuZi9aeGTw
for { next := getNext(); ... }. Fico curioso para saber qual é a vantagem de escreverfor next := range getNext { ... }rangeouswitchcom canais e rodar uma goroutine que empurra valores para o canal. Ainda não estou convencido de por que corrotinas são necessáriasDesta vez, não parece um raciocínio cuidadoso com uma boa solução 80/20, mas algo como: “para fazer isso direito, vamos precisar de corrotinas; então vamos simplesmente colocá-las”
Ao adicionar generics, houve uma reflexão realmente longa e profunda, e foi apresentada uma solução de compromisso inovadora e muito bem equilibrada
Aqui, eu esperava uma abordagem como “adicionar um recurso às goroutines para permitir controle em certas situações”. Isso teria parecido melhor do que “vamos seguir o Rust de forma abrangente neste problema e simplesmente adicionar isso”
Uso Go profissionalmente há vários anos, mas não quero que ele vire algo parecido com Twisted / Tornado / outros frameworks do Python
A palavra-chave
goevita bem o bastante o doloroso problema da coloração de funçõesEm contextos de alta performance, às vezes você quer fazer coisas como sharding de dados por núcleo de CPU, mas esta proposta não coça essa coceira
async/awaitpara GoCorrotinas preencheriam outro espaço, mais próximo dos geradores do Python. Muitas vezes podem reduzir bastante o uso de memória e a complexidade do código em lugares onde é preciso ligar pipelines de componentes componíveis. O design parece totalmente síncrono
Context, isso é literalmente coloração de funçõesClaro, é quase certo que algum desenvolvedor inteligente em algum lugar do mundo vai criar algo idiota com isso, e que isso vai ficar muito popular. Meu palpite é uma abstração única para goroutines e corrotinas
Em geral, código relacionado a goroutines parece bagunçado por causa dos canais, e não sei como deixá-lo mais “limpo”. Eu realmente agradeceria dicas de padrões já validados como manuteníveis
Sistemas multitarefa nos deram processos
Mas eles eram pesados demais
Então surgiram as threads, processos que compartilham o espaço de endereçamento, a tabela de arquivos e algumas outras coisas. O escalonador consegue alternar entre threads com mais facilidade do que entre processos, e o compartilhamento de dados entre threads não exige serialização
Mas isso também era pesado demais
Então surgiram as threads em espaço de usuário. São threads lógicas de execução conduzidas inteiramente pelo runtime no espaço de usuário. O runtime coloca hooks de escalonamento em todas as funções de entrada/saída da biblioteca padrão ou preempta threads lógicas por meio de APIs do sistema, como sinais Unix. Não exigem troca de contexto em nível de sistema e podem ser bem pequenas
Mas isso também era pesado demais
Então surgiram as corrotinas. Elas permitem que o programador defina “threads” lógicas que interagem cooperativamente entre si. Não presumem a existência de um escalonador. O programador escreve o próprio loop de eventos ou chama o loop de eventos de uma biblioteca a partir de uma thread lógica “de verdade”
Fico curioso para saber o que virá a seguir. Do ponto de vista de [processos sequenciais comunicantes][1], talvez corrotinas cooperativas sejam o nível mais baixo a que se pode chegar
[1]: https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf
Seriam necessárias semânticas que deem suporte à concorrência. Por exemplo, iterações deveriam ser, por padrão, não ordenadas; isso também depende de análise de fluxo do programa inteiro, mas não vejo nenhum obstáculo de princípio
A Microsoft não pesquisou uma linguagem assim alguns anos atrás? Também há o ParaSail, feito por alguém da comunidade Ada. Fico curioso sobre o que aconteceu com esses projetos e se ninguém os usa
Eu achava que o ponto central de green threads era obter um bom escalonamento cooperativo sem usar uma palavra-chave como
yielddo PythonAchei que a decisão de design de Go de
inserir pontos de retomada nos pontos de chamada e em locais específicosera um compromisso muito bomEstão expondo cada vez mais controle próximo do hardware. A partir de certo ponto, não sei se isso não é apenas recriar Zig. O próximo passo é um coletor de lixo opcional?
yielderesumenão são palavras-chave, mas variáveis comuns. São apenas referências a closures comuns nomeadas assim para fins didáticosO ponto peculiar é a parte de criar e usar callbacks de cancelamento. Não usei muito Go, então não sei se é uma melhoria de desempenho para coletar mais rapidamente o estado de iteradores descartados, ou se é necessário porque Go não faz garbage collection de uma goroutine esperando em um canal quando essa goroutine tem a única referência a esse canal
Em Lua, nada disso é necessário. Corrotinas/threads também passam por garbage collection como qualquer outro objeto, então, quando todas as referências desaparecem, elas são coletadas mesmo que a última operação tenha sido
yield, e não o retorno da função de entradaGOGC=offMais detalhes sobre GOGC: https://dave.cheney.net/tag/gogc
Acho que eu preferiria que corrotinas entrassem como suporte da linguagem em vez de biblioteca
Imagino algo como
x := co func(){ var z int; for { z++; yield z } }, ou algo equivalenteÉ legal que isso seja possível só com Go puro, e entendo a atração de oferecê-lo como um pacote da biblioteca padrão com um runtime otimizado, em vez de complicar a especificação da linguagem. Afinal, se é possível em Go puro, outras implementações também podem fazer bootstrap rapidamente
Como alguém que usa Go todos os dias no
$work, eu receberia bem qualquer uma das duas opções, mas preferiria que fosse embutido na linguagem. As primitivas de concorrência de Go sempre foram um ponto forte, então faz sentido seguir nessa direçãoEm uma solução de biblioteca, seria preciso matar a corrotina ou matar o programa. Se você quer que a pilha cresça de forma transparente, só o código gerado pode fazer isso, porque ele precisa monitorar o uso da pilha e aumentá-la quando necessário. Pelo que sei, goroutines têm algo assim
Talvez uma solução de biblioteca também possa colocar uma página de guarda no fim da pilha. Ao atingi-la, o tratador de erro poderia tentar expandir a pilha. Mas, se houver ponteiros guardados para variáveis de pilha, provavelmente não funcionaria
yieldàs funções para interagir melhor com o sistema de tipos atualA ideia seria que uma função que cede valores tenha um chamado parâmetro
yield, e que só possa ceder dentro de funções com a mesma assinatura de cessão ou sem assinaturaO exemplo poderia virar algo como
x := func(:z int) { for { z++; :- z } }.:adiciona a assinatura de cessão, e:-cede o valorUma função que só cede valores
Xprecisaria ter apenas: Xou: name X. Para receber, ao ser retomada, um valor do tipoY, a assinatura mudaria para:[Y] Xou:[Y] name XA aceitação deve ser fraca. Em um lugar que espera uma função que é retomada com
Ye cedeX, também se deve aceitar uma função que apenas cedeX, sem retomadaSe funções especiais do pacote
cofornecerem os recursos deresumeeNew, daria para manter o estilo de Go. A sintaxerangepoderia ser estendida para passar valores de retomada com-:, e, se nenhum valor for passado, a retomada ocorreria com o valor zero padrãoNão gostei muito. Pelos exemplos, parece que a linguagem fica muito mais difícil de ler e acompanhar. Claro, talvez seja culpa da minha cabeça e dos meus preconceitos
Além disso, também não parece permitir fazer algo que não seja possível com os canais bloqueantes ou estado atuais
Já usei iteradores parecidos com os deste texto para evitar alocações em caminhos críticos de código. Com esta abordagem, esse tipo de código ficaria muito menos estranho. Especialmente junto com a mudança de linguagem para iteradores com
rangeque deve chegar em breveCanais são lentos sem muito motivo quando não estão encapsulando operações bloqueantes
Fico amargurado ao ler os comentários
Muita gente considera corrotinas e green threads quase a mesma coisa, mas ambas têm vantagens e desvantagens
É triste que, na comunidade Go, a ausência de iteradores possa ser aceita. Parece que, em nome da simplicidade, rejeitam deliberadamente qualquer recurso que possa tornar a linguagem sequer um pouco mais complexa. Ainda assim, pelo menos voltaram atrás na posição sobre genéricos
Isso reforça a ideia de que Go realmente não é a minha linguagem
Hoje eu os uso um pouco, porque a conveniência sintática no ponto de chamada é um pouco melhor. No ponto de definição, como esperado, continuam feios; ainda assim, a sintaxe de Go é uma das melhores que já vi entre outras linguagens
No fim, acho que se perdeu bastante simplicidade só para calar as reclamações de que “não há genéricos”. Não foi uma boa troca
O pacote
coroproposto no texto será adicionado exatamente como está? Pode ser, mas provavelmente não exatamente assim. Algo parecido será adicionado? Se eu pudesse apostar dinheiro, diria que simQuanto tempo vai levar? Acho que no mínimo 1 ano, ou seja, por volta do lançamento do Go 1.23, em agosto de 2024. Pode demorar um pouco mais. Acho difícil ser muito menos que isso
Genéricos foram aceitos pela comunidade porque são totalmente retrocompatíveis com o código existente e podem ser ignorados com segurança se você não precisar deles
Sem surpresa, a maior parte do código Go continua fazendo exatamente isso. Exceto por vários tipos de “coleções”, não é tão fácil encontrar usos práticos para genéricos. Para começo de conversa, a maior parte do código não precisa lidar com mais de um tipo; passar de dois tipos já é algo raro
Na verdade, a adição de genéricos mostrou para a comunidade mais ampla como esses recursos aclamados que as pessoas exigem como “essenciais” são, na prática, muito menos necessários em uma linguagem excelente e amplamente aceita
Parece positivo que só agora estejam prestando atenção a linguagens de programação como CLU
Por outro lado, pela minha experiência com corrotinas em .NET e C++, e com Active Object em Symbian C++ e Active Oberon, não tenho certeza de que isso realmente valha a pena ser adicionado ao Go
Como a equipe .NET também reconheceu na BUILD deste ano, se pudessem voltar no tempo, teria sido melhor deixar o runtime cuidar disso no estilo Go. Muitos desenvolvedores ainda têm dificuldade para entender async/await
Não sei se isso é realmente necessário. Na maioria dos casos em Go, goroutines dão conta, e para a semântica de
yield/resumebastam 2 canais bloqueantesParece adicionar complexidade pela complexidade, e nem está claro se isso realmente acrescenta alguma capacidade nova que já não existisse em Go
Como comparação, em uma apresentação recente fiz uma demonstração iniciando 1 milhão de threads em Elixir (BEAM VM), enviando uma mensagem
"Hello!"para todas elas e, depois, cada thread esperava por um tempo aleatório entre 0 e 2 segundos antes de devolver"Process received message !"Ao mesmo tempo, deixei o Erlang observer aberto ao lado para observar consumo de CPU e memória, e a rapidez da recuperação após a coleta de lixo
O maior gargalo aqui é a capacidade do terminal de acompanhar, mas o observer parece refletir bem a situação real
https://www.youtube.com/watch?v=yxyYKnashR0
Código usado: https://gist.github.com/pmarreck/4cc8f2f55a561ebce2012085a3a...
Esse recurso existe embutido em Erlang, e portanto também em Elixir, desde os anos 1980. Muita gente já ouviu falar do modelo de atores ou da implementação “lendária” de Erlang, mas não sei quantas pessoas já viram isso na prática com ferramentas de monitoramento abertas ao lado
Seria bom se Go oferecesse esse tipo de suporte no nível da linguagem, mas a implementação de threads da BEAM VM é extremamente eficiente em recursos tanto na criação quanto no consumo em tempo de execução, e ainda combina isso com a facilidade de concorrência que vem de permitir apenas valores imutáveis; acho que será muito difícil alcançá-la