Async Rust nunca saiu do estado de MVP
(tweedegolf.nl)- Async Rust permite executar código independente de executor tanto em servidores quanto em microcontroladores, mas a máquina de estados gerada pelo compilador faz o tamanho do binário crescer de forma especialmente visível em sistemas embarcados
- Mesmo um exemplo simples como
bar(), com 2 pontos deawait, gera 360 linhas de MIR e os estadosUnresumed,Returned,Panicked,Suspend0,Suspend1, enquanto a versão síncrona precisa de apenas 23 linhas - Se, ao fazer poll de um future já concluído novamente, ele passar a retornar
Poll::Pendingem vez depanic, é possível cumprir o contrato sem comportamento unsafe, e em experimentos isso reduziu o tamanho do binário em 2% a 5% em firmware embarcado - Mesmo
async { 5 }, semawait, hoje ainda gera uma máquina de estados com 3 estados básicos, mas otimizar isso para sempre retornarPoll::Ready(5)reduziu o tamanho de binários embarcados em 0,2% - O Project Goal proposto quer avançar no compilador com a remoção do panic após conclusão em modo release, eliminação da máquina de estados em blocos async sem await, inline de futures com um único await e fusão de estados idênticos
O problema de inchaço no nível do compilador em Async Rust
- Async Rust permite executar código independente de executor ao mesmo tempo em servidores e microcontroladores, mas em microcontroladores pequenos o aumento no tamanho do binário é especialmente perceptível
- O blog do Rust apresentou async/await como uma abstração de custo zero, mas na prática async gera bastante inchaço, e o mesmo problema também existe em desktop e servidor, só aparece menos por causa da abundância de memória e recursos de computação
- Após as soluções alternativas para evitar inchaço ao escrever código async, foi submetido um Project Goal para resolver o problema no compilador
- O problema de futures maiores do que o necessário e com cópias excessivas está fora do escopo
- Esse problema já é conhecido, e há um PR aberto tratando parte disso: https://github.com/rust-lang/rust/pull/135527
Estrutura do future gerado
- O código de exemplo faz
foo()retornarasync { 5 }, ebar()executafoo().await + foo().await- Exemplo no Godbolt: godbolt
- Como
bartem 2 pontos de await, a máquina de estados precisa de pelo menos 2 estados, mas na prática vários estados extras são gerados - O compilador Rust pode despejar MIR em várias passagens, e a passagem
coroutine_resumeé a última passagem de MIR específica para async- Async ainda existe no MIR, mas não no LLVM IR, então a transformação de async em máquina de estados acontece na fase de MIR
- A função
bargera 360 linhas de MIR, enquanto a versão síncrona usa apenas 23 linhas - O
CoroutineLayoutemitido pelo compilador é, na prática, um conjunto de estados em forma de enumUnresumed: estado inicialReturned: estado concluídoPanicked: estado após panicSuspend0: primeiro ponto de await, armazenando o future defooSuspend1: segundo ponto de await, armazenando o primeiro resultado e o segundo future defoo
Future::pollé uma função segura, então chamá-la novamente depois que o future já terminou não pode causar UB- Hoje, após
Suspend1, ele retornaReadye muda o future para o estadoReturned - Se houver novo poll nesse estado, ocorre panic
- Hoje, após
- O estado
Panickedparece existir para impedir novo poll de um future depois que uma função async entrou em panic e isso foi capturado comcatch_unwind- Depois de um panic, o future pode ficar em estado incompleto, então fazer poll novamente poderia levar a UB
- Esse mecanismo é muito parecido com poisoning de mutex
- Essa interpretação do estado
Panickedtem cerca de 90% de confiança, já que é difícil encontrar documentação definitiva
É realmente necessário dar panic ao fazer poll após a conclusão?
- Hoje, um future no estado
Returnedentra em panic, mas isso não é estritamente necessário- A única exigência é não causar UB
- Panic é relativamente caro e adiciona um caminho com efeitos colaterais difíceis de remover por otimização
- Se um future já concluído passar a retornar
Poll::Pendingquando receber novo poll, ainda será possível cumprir o contrato do tipoFuturesem comportamento unsafe - Ao modificar o compilador para testar essa abordagem, foi observada uma redução de 2% a 5% no tamanho do binário em firmwares embarcados com async
- Foi proposto oferecer esse comportamento como uma opção, de forma parecida com
overflow-checks = falsepara overflow de inteiros- Em builds de debug, o panic continuaria para expor imediatamente o comportamento incorreto
- Em builds de release, seria possível obter futures menores
- Ao usar
panic=abort, pode haver margem para remover o próprio estadoPanicked, mas o impacto disso ainda precisa de análise adicional
Mesmo sem await, uma máquina de estados sempre é gerada
foo()retorna apenasasync { 5 }, então a forma otimizada manualmente seria um future sem estado que sempre retornaPoll::Ready(5)- Porém, no MIR gerado pelo compilador, ainda existem os 3 estados básicos:
Unresumed,ReturnedePanicked- Ao fazer poll, o discriminante do estado atual é verificado e ocorre um desvio condicional
- Se houver novo poll após a conclusão, acontece um panic com
`async fn` resumed after completion
- Nesse caso, seria possível otimizar para não criar máquina de estados e simplesmente retornar
Poll::Ready(5)toda vez - Ao aplicar isso experimentalmente no compilador, o tamanho do binário embarcado caiu 0,2%
- A redução não é grande, mas por ser uma otimização simples, talvez ainda valha a pena
- Essa otimização muda ligeiramente o comportamento, mas só afeta executores que violam o contrato
- Hoje o compilador entra em panic em polls posteriores
- Após a otimização, o future sempre retorna
Ready
Só o LLVM não basta
- Mesmo que a saída de MIR seja ineficiente, em alguns casos o LLVM consegue limpar tudo, mas as condições são limitadas
- O future precisa ser simples o suficiente
- É preciso usar
opt-level=3
- Quando o future fica mais complexo, o LLVM não consegue eliminar tudo, e em código async idiomático em Rust os futures tendem a ser profundamente aninhados, então a complexidade cresce rápido
- Em ambientes como embarcados ou wasm, onde otimização por tamanho é comum, o LLVM não consegue resolver isso sozinho
- Exemplo no Godbolt: https://godbolt.org/z/58ahb3nne
- No assembly gerado, o LLVM sabe que
fooretorna 5, mas não consegue otimizar a resposta debarpara 10 - A chamada da função
polldefootambém permanece - O motivo são caminhos potenciais de panic que o compilador não consegue descartar completamente
- O LLVM não sabe que
foona prática só será chamado uma vez e não entrará em panic
- No assembly gerado, o LLVM sabe que
- Se os ramos de panic forem comentados no IR, a otimização melhora: https://godbolt.org/z/38KqjsY8E
- Em vez de esperar uma otimização posterior do LLVM, o compilador precisa fornecer uma entrada melhor para ele
O inline de futures não está funcionando bem
- Inline é importante porque habilita passagens posteriores de otimização, mas os futures gerados em Rust hoje não são inlinados nessa fase inicial
- Depois que cada future ganha sua implementação, LLVM e linker até têm chance de fazer inline, mas por causa dos problemas anteriores esse momento já é tarde demais
- A oportunidade de inline mais direta é quando
bar()apenas faz algo comofoo(blah).await- Esse padrão aparece com frequência ao criar abstrações com traits
- Hoje o compilador cria uma máquina de estados para
bare chama a máquina de estados defoodentro dela - De forma mais eficiente,
barpoderia ser o próprio future defoo
- Quando há preâmbulo e pós-processamento, o caso fica mais complexo
- Exemplo:
bar(input)criablahcominput > 10, depois fazfoo(blah).awaite aplica* 2ao resultado - Isso é comum ao transformar funções async para outra assinatura, especialmente em implementações de trait
- Exemplo:
- Mesmo nessa forma,
barnão precisa de estado async próprio- Não há dados preservados além do único ponto de await, exceto o valor capturado por
foo - Ainda assim,
barnão pode simplesmente se tornar o própriofoo, e pode depender da maior parte do estado defoo
- Não há dados preservados além do único ponto de await, exceto o valor capturado por
- Numa implementação manual,
BarFutpoderia ter os estadosUnresumed { input }eInlined { foo: FooFut }- No primeiro poll, ele executaria o preâmbulo, criaria
foo(blah)e mudaria paraInlined - Depois disso, aplicaria o pós-processamento ao resultado de
foo.poll(cx)
- No primeiro poll, ele executaria o preâmbulo, criaria
- Se fosse possível executar código antecipadamente até o primeiro ponto de await, também daria para remover o estado
Unresumed, mas isso não pode mudar porque há a garantia de que o future não faz nada antes de receber poll - Se fosse possível consultar propriedades do future durante o poll, mais otimizações de inline poderiam acontecer
- Por exemplo, se fosse sabido que o future sempre retorna ready no primeiro poll, o future chamador não precisaria criar estado para aquele ponto de await
- Aplicar esse tipo de otimização recursivamente permitiria fundir muitos futures em máquinas de estados bem mais simples
- Pela estrutura atual do
rustc, cada bloco async parece ser transformado isoladamente e os dados relevantes não são preservados depois, o que impede esse tipo de consulta - O inline de futures ainda não foi testado experimentalmente, mas há expectativa de grande benefício para tamanho de binário e desempenho
Fusão de estados idênticos
- Cada ponto de await em um bloco async adiciona um estado extra à máquina de estados
- O código abaixo é natural, mas como os dois ramos fazem await da mesma função async, surgem 2 estados idênticos
CommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- Nesse caso, o
CoroutineLayoutpassa a ter_s0e_s1, ambos armazenando o mesmo tipo de coroutine desend_response, além dos estadosSuspend0eSuspend1 - O MIR dessa função tem 456 linhas, e muitos blocos básicos são essencialmente duplicados
- Se o código for refatorado manualmente para primeiro calcular apenas o valor da resposta e depois chamar uma única vez
send_response(response).await, os estados duplicados desaparecemCommandId::Avira123CommandId::Bvira456- Depois disso,
send_response(response).await
- Após a refatoração, o
CoroutineLayoutpassa a ter apenas um future armazenado e sobra só o estadoSuspend0 - O comprimento total do MIR cai para 302 linhas, eliminando a duplicação
- Portanto, parece útil ter uma passagem de otimização que detecte caminhos de código e estados equivalentes para fundi-los
- Essa otimização provavelmente combina bem com uma passagem de inline de futures
Links dos experimentos e benchmarks adicionais
- Aplicando os dois experimentos juntos, houve cerca de 3% de ganho de desempenho em um benchmark sintético x86 usando o executor
smol - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Pedido de apoio ao Project Goal
- Esse trabalho foi submetido como Project Goal para ser realizado no compilador: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- Sem financiamento, é difícil avançar muito, então é necessário apoio parcial ou integral de empresas ou organizações que possam se beneficiar desse trabalho
- O contato é
dion@tweedegolf.com - O escopo do trabalho e o volume de financiamento necessário são flexíveis, mas a estimativa é que €30k permitiriam concluir tudo ou uma parte substancial
2 comentários
Comentários no Hacker News
Concordo que o título é um pouco exagerado, mas o texto foi bem escrito e transmite bem a ideia
Ainda não tenho experiência suficiente com async em Rust para ter uma opinião forte, mas algumas coisas me chamaram a atenção
O lado bom é que dá para ter um runtime explícito. Em vez de contaminar o projeto inteiro com async, dá para manter o padrão como síncrono e usar o runtime só nas “bordas” de E/S
Isso funcionou bem no projeto em que estou trabalhando, e parece bem parecido com a estratégia que o Zig adota para código de E/S. Nesse caso, o problema da cor das funções também ficou em grande parte resolvido, e como era preciso separar com rigor o código de E/S do código centrado em CPU, um runtime explícito de E/S pareceu natural
O lado ruim é que o ecossistema inteiro parece depender demais de tokio. É como se o GC do Java fosse opcional, mas na prática todo mundo usasse o mesmo runtime de GC de terceiros, e qualquer biblioteca que você importasse te obrigasse a usar esse runtime. Esse tipo de dependência central não é saudável
Os requisitos de runtime async em processadores de workstation e em ambientes como o RP2040 são muito diferentes. Ainda assim, como é possível trocar o backend, ao escrever código async de E/S para um microcontrolador ARM M0 pequeno, usando o embassy, que é um runtime voltado para embarcados, o código acaba parecendo quase igual ao usado em outros ambientes
Como usa os mesmos traits e interfaces, dá para se preocupar menos com os detalhes do runtime. Comparado a usar um RTOS pequeno ou montar o próprio ambiente async, isso é muito bom
O que aprendi escrevendo código async com embassy também pode ser levado para outras áreas
Mesmo não fazendo parte da biblioteca padrão, tokio é bem mantido, então a situação atual me parece aceitável. Na verdade, se entrasse na biblioteca padrão, eu até me preocuparia que isso dificultasse o uso de outros executores e tornasse mais difícil portar a biblioteca padrão para outras plataformas
Claro, pode ser que essa preocupação não tenha fundamento
Logging hoje está mais organizado em torno de slf4j, mas ainda há bibliotecas que usam outras coisas, e utilitários comuns começaram com Apache Commons, enquanto hoje muita coisa usa Guava
JSON se organizou em parte em torno do Jackson, mas Gson e Simple-json ainda são comuns, e as anotações de nulidade também nunca foram oficializadas: passaram de distribuições não oficiais do JSR-305 para o checker framework e mais recentemente estão migrando para o JSpecify
Esses elementos básicos precisam ser fornecidos pela linguagem para evitar fragmentação e a proliferação de bibliotecas que viram padrões de fato
Escrever bibliotecas independentes de executor não é tão difícil, mas exige atenção constante, e isso nem sempre é seguido pela maior parte da comunidade
Excelente texto. Gosto desse tipo de análise profunda de otimização, e espero que as metas do projeto avancem bem
Já tive a impressão de que o compilador muitas vezes não investe tanto esforço em otimizar casos “triviais”
Dito isso, o título é dramático demais para o conteúdo. Eu teria clicado mesmo se fosse “Async Rust Optimizations the Compiler Still Misses”
Hoje já dá para usar async em traits e closures, mas isso é uma atualização do sistema de tipos, não uma mudança na mecânica do async em si. O Waker também ficou um pouco mais fácil de lidar, mas isso é mais uma melhoria de std/core
Pelo que entendo, as pessoas que fizeram o landing do async Rust acabaram sofrendo bastante burnout e reduziram a participação, e quase ninguém assumiu depois. Ainda assim, é bem animador ver que o pessoal do Google abriu um PR para otimizar o layout de memória das variáveis capturadas
Eu e meus colegas usamos bastante async, então talvez a gente mesmo precise fazer isso, ou pelo menos começar. Parece aquele tipo de “grátis” que é grátis no mesmo sentido de ganhar um filhote
Então sim, o título é um pouco caça-clique, mas mesmo assim não pretendo voltar atrás nele
O autor parece obcecado com o overhead de funções triviais. Ele se incomoda com o overhead dos estados “panic” e “returned”, mas isso não é um grande problema
A maioria dos blocos async úteis é grande o bastante para que o overhead de casos de erro se dilua
Pode haver um ponto válido sobre falta de inlining. Mas, em geral, o que limita grandes quantidades de atividades é o espaço de estado exigido por cada uma delas
Async no geral parece uma ideia pouco madura. Código comum já era assíncrono
Se você precisa esperar uma tarefa async, a thread dorme até ficar pronta, e o kernel abstrai isso para você. Só que as pessoas não gostaram de estruturar o código em torno de threads lógicas, então adicionaram um sistema de callbacks para eventos, e depois perceberam que callbacks são difíceis de raciocinar e que controle sequencial é melhor
Então eu diria que threads são o modelo de programação correto
Agora os runtimes de linguagem preferem “green threads” por motivos de portabilidade e desempenho, mas a maioria das linguagens não oferece isso direito. Em vez disso, surgem problemas como a cor async/non-async, escalonamento, prioridades e ausência de preempção. É um modelo de processos e escalonamento pior do que nos anos 1970
Mesmo código async muitas vezes é escrito de forma que não maximiza a concorrência expressável. Por exemplo, em vez de “execute N tarefas de E/S todas ao mesmo tempo”, escreve-se algo como “para cada tarefa X,
await process(x)”Mas no mundo das threads esse problema de concorrência é ainda pior. Threads são pesadas demais por natureza para expressar concorrência de forma eficiente, e não há como otimizá-las nessa direção
Isso não é uma lição nova. Executores com work stealing já eram conhecidos há muito tempo por terem latência muito menor e P99 mais consistente do que threads tradicionais. Foi por isso que a Apple criou o GCD no começo dos anos 2000
Threads não fornecem ao escalonador do kernel informações mais ricas de que ele precisa para entender a carga de trabalho, e threads de kernel são um mecanismo pesado demais para conseguir concorrência fina. Em cargas de E/S ou mistas isso é ainda pior
Nem todo programa precisa desse nível de desempenho, mas é muito mais fácil atingir um patamar mais alto de performance com o mesmo esforço, e de fato dá para alcançar latência e throughput que abordagens tradicionais têm dificuldade de acompanhar
Um sinal de que async está indo na direção certa também aparece em io_uring. A abordagem de E/S de alto desempenho do kernel com io_uring é completamente diferente do modelo tradicional de threads e syscalls, e o tratamento de conclusão é muito mais próximo da concorrência async. O problema é que async/await sozinho não tem cores suficientes para expressar as relações entre tarefas async, então é mais difícil explorar isso por completo
Da última vez que mexi com código de corrotinas/escalonamento, criar uma thread que terminava imediatamente e dar join nela levava cerca de 200µs, enquanto criar, escalonar e esperar uma green thread própria levava cerca de 400ns
Não é preciso esperar 10 anos até alguém projetar outro framework async absurdamente complexo. Em qualquer linguagem de sistemas, com umas 20 linhas de assembly dá para fazer sua própria green thread/corrotina com stack
Otimizar código centrado em largura de banda é um problema de desenho de escalonamento. No modelo clássico multithread, você tem controle limitado sobre o escalonamento; no modelo async, você consegue controlar quase perfeitamente
Um agendamento async bem otimizado é muito mais rápido que uma arquitetura multithread equivalente na mesma carga centrada em largura de banda, a ponto de nem haver comparação
Hoje, grande parte do código de alto desempenho é centrado em largura de banda, e async existe para tornar mais fácil otimizar esse tipo de carga
Ao testar processamento concorrente e verificar se lida corretamente com condições de corrida, callbacks tornam isso muito mais fácil porque você pode controlar o escalonamento. Como cada callback representa uma unidade separada, dá para ver quais eventos podem ser reordenados e analisar mais facilmente ordens diferentes
Já com threads, é fácil ignorar a ordem e deixar de pensar em quando a complexidade de outras threads pode afetar a thread atual. Não é exatamente simplicidade; é mais uma simplificação
Além disso, é difícil testar de fato cenários concorrentes alterando a ordem real das coisas, a menos que você introduza barreiras artificiais para parar threads, ou troque a E/S por stubs e passe mocks com callbacks que controlem a ordem
O problema dos callbacks é que a pilha de chamadas capturada não é a pilha lógica de chamadas. Sem bibliotecas/runtimes que trabalhem para tornar a pilha significativa, é preciso uma boa definição de erro
Claro, também é possível misturar os dois paradigmas e ficar só com as desvantagens de ambos
Se o objetivo principal do Rust é segurança, eu não entendo por que existe panic. Deveria ser possível provar que não há nenhum caminho no código capaz de causar panic
Passei a semana inteira olhando isso, e é muito difícil fazer um programa com garantia de que nunca dará panic. Pelo que entendi, o handler de panic ocupa algo como 300KB, e a única forma de excluí-lo é fazer com que no momento da compilação não exista absolutamente nenhum caminho no código que possa causar panic. Verificar depois da compilação se o binário incluiu um handler de panic parece um hack
Dá para proibir
unwrape outras operações que causam panic com lint, mas se existisse um subconjunto no-panic de Rust, boa parte dos problemas tratados neste texto desapareceriaÉ frustrante lidar com uma linguagem em que há tantas operações que teoricamente podem causar panic, mesmo em situações que na prática só aconteceriam com algo no nível de bit flip. Isso vale tanto para provar que um array não está vazio quanto para lidar com async
No fim, você acaba enchendo o código de tratamento de erro para situações que nunca vão acontecer, ou usando estruturas estranhas como o padrão de lista não vazia com primeiro campo e resto da lista separados. E mesmo essa estrutura adiciona seu próprio inchaço
O trabalho para ampliar usos baseados em prova, incluindo provas de que um array não está vazio, também está avançando aos poucos
Se não existisse panic e fosse preciso continuar executando em qualquer situação, então em casos como corrupção de memória que quebra invariantes você teria de colocar muito tratamento de erro em todo lugar que verifica invariantes para tentar se recuperar
Isso seria exatamente o mesmo tipo de problema que você está apontando: uma quantidade enorme de tratamento de erro para situações que quase nunca vão acontecer
Cansa ver essa postura de querer que as ferramentas tornem tudo infalível sem querer fazer nada por conta própria. Quer-se uma API fácil; se não for fácil o bastante, quer-se “programar” contêineres Kubernetes com YAML; se isso também não for fácil o bastante, quer-se um serviço de hospedagem com cliques da GCP ou da Amazon
No fim, parece menos programação e mais desejo de consumir apps infalíveis, e esse modo de vida só existe em relação simbiótica com quem constrói as coisas
Esse tipo de discussão feia, mas necessária já acontece no C++ há algum tempo
Desde que async foi introduzido em Rust eu não gostava da sua natureza contagiosa
Quero que Rust dê certo, e se mais gente assim aparecer, o futuro do Rust pode ficar mais promissor
Comecei recentemente a trabalhar com async em Rust, e o principal problema que estou enfrentando agora é duplicação de código
Toda função que eu quero que suporte tanto uma API assíncrona quanto uma API bloqueante precisa ser escrita em duplicidade. Seria ótimo ter
maybe-asyncTentei contornar isso olhando crates como maybe-async e bisync, mas todos tinham problemas ou restrições fortes
asyncouconstNo momento, a melhor opção para escrever código que queira viver tanto no mundo síncrono quanto no assíncrono é sans-io. Thomas Eizinger, do Fireguard, escreveu um bom texto sobre esse padrão[1]
Esse padrão não só resolve de forma elegante a questão sync/async, como também facilita testes e abre caminho para técnicas como DST[2]
Também escrevi um texto sobre isso[3], destacando que o problema vai além de async versus sync e inclui a questão mais ampla de diferentes executores
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
asyncjá émaybe-asyncA diferença entre
fn -> voidefn -> Futureé que a primeira termina imediatamente, enquanto a segunda pode terminar só depoisSe você quer executar uma função async de forma bloqueante, basta usar um executor bloqueante
Gostei deste texto porque ele até me fez olhar para as metas do Rust para 2026
Meu time usa Rust, mas não tivemos necessidade de mergulhar tão fundo para fazer o que precisávamos. Mesmo assim, é divertido ver uma linguagem evoluindo desde a base com tanto feedback da comunidade
No C++ eu não tinha muito essa sensação, e também não sei bem como isso funciona em outras áreas
Só acho uma pena que cada meta pareça precisar de financiamento específico, o que dá um ar meio Kickstarter. Fico curioso se esse é o melhor modelo que encontraram até agora
Metas de projeto são um sistema em que uma pessoa ou um pequeno grupo expressa que quer trabalhar em algo e pede aos voluntários do projeto Rust um compromisso contínuo de suporte, como revisão de código ou resposta a perguntas
Isso não significa que o projeto Rust em si definiu essa meta, nem que necessariamente a apoia
Então não é correto ver isso como um roadmap oficial do Rust; é mais preciso entender como “há contribuidores interessados em trabalhar nesta área”
Infelizmente, quando uma tecnologia se estabelece comercialmente, parece que esse tipo de coisa acaba acontecendo. Também é difícil culpar grandes patrocinadores por financiarem apenas as partes que lhes interessam
Felizmente, pelo que sei, uma parte significativa do financiamento da TweedeGolf vem do governo holandês
Funcionalidades novas são algo que dá para “vender”. Custam dinheiro para serem construídas, mas resolvem problemas reais, e se o custo desse problema for maior que o custo de desenvolver a funcionalidade, empresas em geral aceitam pagar
Manutenção é mais difícil, mas hoje já existem fundos para mantenedores. Um exemplo é o fundo da RustNL: https://rustnl.org/maintainers/
Esses fundos se destinam a trabalhos mais amplos e contínuos, sustentados por várias organizações contribuindo um pouco cada uma
Não sei se é o melhor modelo, mas pelo menos parece funcionar até certo ponto
Se você ler a documentação de Rust Async e Tokio, está bem explicado por que não se deve colocar partes intensivas em CPU na stack async, como usar de forma eficiente ferramentas básicas como
std::sync::Mutexdentro de blocos async, e como ligar código síncrono a código asyncMuito código não segue essas orientações porque não se importa com eficiência, ou não precisa dela. Mas há muitos projetos que valorizam desempenho e eficiência, e quando o código vai para produção as armadilhas ficam evidentes. ScyllaDB é um exemplo
LLM também não ajuda. Faz tudo virar async até
main, usa as ferramentas básicas erradas e não desenha o sistema direitoA fusão de estados duplicados, ou seja, o padrão de puxar o
matchpara fora dos ramos com await, como no exemploprocess_command, é provavelmente a coisa mais fácil que qualquer um pode aplicar hoje em código async existenteNão exige trabalho do compilador, só refatoração
Sobre a parte de que “Futures não são facilmente inlineadas”, na linguagem de programação que eu criei escrevi um passe customizado para fazer inline de chamadas de funções async dentro de funções async
Em geral funciona bem e consegue eliminar parte do boilerplate, mas o tamanho do binário resultante aumenta bastante
Tecnicamente, Rust também poderia fazer a mesma coisa
Opiniões no Lobste.rs
Foi um texto muito mais construtivo do que eu esperava só pelo título
Espero que quem queira trabalhar nisso receba o apoio necessário
É bom ver esse problema sendo tratado. Já vi algumas vezes gente dizendo que hoje o rustc passa código demais para o LLVM e espera que o otimizador resolva tudo, e este texto em especial também está pedindo financiamento para esse trabalho
Meu Deus, eu fui burro
Sempre pensei que async fosse inerentemente “inchado”, já que de qualquer forma precisa de runtime, rastreamento de tarefas e polling para verificar conclusão. Então esse overhead nunca é zero
Eu entendia que a “abstração sem custo” aqui era sobre o recurso da linguagem, separada do runtime adicional
Nunca nem me ocorreu olhar o que o rustc emite antes de passar para o LLVM
Para quem não está familiarizado com async Rust:
Isso é realmente verdade. Até uma árvore de chamadas async aninhadas, quando recebe otimização máxima, acaba consolidada em uma única struct com máquinas de estado dentro. É uma abordagem realmente engenhosa
Em build de release, chegar a esse caso gera algum tipo de deadlock? Ou pode haver vazamento por causa de tarefas esperando trabalhos que ficam sempre em
Pending?Não dá para fazer polling incorreto usando
.awaitAlgumas ideias me ocorreram:
panic=unwind. Tirando alguns test harnesses, quase nunca vi vantagens sobrepanic=abortque compensem o custo. Mesmo para test harnesses, no Linux parece que daria para aplicar uma escolha parecida, usandoclonede forma meio obscura para fazerwaitna thread executora em vez depthread_join. Posso estar errado nessa parteO link morreu agora há pouco para mais alguém?
Edit: o post do blog aparece por meio segundo e depois vai para uma página 404
Edit 2: entrei na lista de posts do blog, cliquei em várias coisas, e até abrindo aquele post a partir da lista vai para uma página 404. Como alguém consegue quebrar assim um blog estático, ou pelo menos que deveria ser estático?
Para constar, acho que segui os mesmos passos de reprodução e não vi nenhum 404. Testei no celular e no desktop, com JavaScript ligado e desligado. Então o que você encontrou talvez fosse mais complexo do que parecia