Talvez Rust não seja adequada para software de espaço do usuário com concorrência em larga escala
(bitbashing.io)- O
async/awaitdo Rust mira a concorrência em larga escala para lidar com dezenas de milhares de conexões, mas entra em conflito com os objetivos do Rust de controle de baixo nível e verificação estática de lifetimes, criando uma experiência de desenvolvimento diferente do Rust “normal” - Threads e canais bastam para muito software, mas em escalas como C10K o custo do modelo de uma conexão por thread fica alto, exigindo tasks em espaço do usuário e escalonamento pelo runtime
- Em Rust
async, os dados precisam ser movidos comoSendou tratados por referências'static, e por causa da natureza contagiosa deasyncessas restrições se repetem por todo o código Arcpode resolver problemas de compilação, mas também torna menos claros os lifetimes de objetos e recursos, enquanto continuam surgindo armadilhas comoasyncrecursivo, a diferença entre future e task, e chamadas blocking que travam threads do runtime- Em Haskell ou Go, “código async” funciona como código comum e o runtime com GC esconde as diferenças, então nesse tipo de programação o controle explícito do Rust pode não funcionar apenas como vantagem
Por que concorrência e paralelismo são necessários
- Programas rápidos têm duas exigências ao mesmo tempo
- precisam usar vários núcleos de CPU para aproveitar o computador inteiro
- precisam continuar fazendo outras coisas enquanto esperam operações lentas, como transmitir mensagens pela internet ou abrir arquivos
- Paralelismo é o problema de executar código ao mesmo tempo em várias CPUs
- Concorrência é uma forma de dividir um problema em partes independentes
- Os dois não são a mesma coisa, mas quando um programa é dividido em partes concorrentes, essas partes podem ser executadas em paralelo e manter os núcleos ocupados
Processos, threads e canais
- Uma forma simples de construir um sistema concorrente é dividir o código em vários processos
- o escalonador do sistema operacional executa fatias de tempo dos processos prontos nos núcleos de CPU disponíveis
- esse modelo também é usado quando comandos de shell são conectados por pipes
- A abordagem com processos tem alto custo de comunicação entre processos
- em muitas implementações, é preciso copiar os dados para a memória do SO e depois trazê-los de volta
- esse custo pode ser reduzido com memória compartilhada, mas isso enfraquece a vantagem de o SO isolar os processos uns dos outros
- Threads compartilham a mesma memória e evitam esse overhead, mas o uso incorreto de ferramentas de sincronização como mutex, condition variable e semaphore pode causar corrida de dados e deadlock
- O modelo Communicating Sequential Processes de Tony Hoare conecta threads com filas ou canais
- as threads não compartilham memória, obtendo um isolamento semelhante ao dos processos
- a entrada e a saída de cada thread ficam expostas por canais, o que facilita raciocínio e depuração
- os próprios canais fazem o papel de sincronização: se estiverem vazios, o receptor espera; se estiverem cheios, o emissor espera
- A biblioteca padrão do Rust inclui std::sync::mpsc::sync_channel
- Para muito software, threads e canais, junto com ferramentas como Rayon para paralelizar loops intensivos em CPU, já são suficientes
Concorrência em espaço do usuário e Rust async
- Em problemas C10K, como um servidor web com dezenas de milhares de usuários conectados ao mesmo tempo, o modelo de associar uma thread a cada conexão encontra limites
- no Linux, cada thread tem um bloco de controle de 4 kB, e a troca de threads exige um context switch que entra no escalonador do sistema operacional
- Para concorrência em larga escala, algumas linguagens criam e gerenciam tasks no espaço do usuário
- o runtime agenda essas tasks em um pool de threads do SO
- normalmente, o pool é configurado para ter uma thread por núcleo de CPU, maximizando o paralelismo
- essa abordagem também é chamada de green thread, lightweight thread, lightweight process, fiber ou coroutine
- O Rust usa o modelo
async/awaitvisto em C# e Node.js- uma
async fnnão retorna diretamente um valor, mas sim um future ou promise cujo resultado é obtido com.await
- uma
- Os futures do Rust são muito pequenos e rápidos graças ao escalonamento cooperativo e ao design stackless
- O Rust tenta oferecer a abstração de futures sem abrir mão de prometer controle de baixo nível ao programador
- tenta verificar estaticamente em tempo de compilação o lifetime de todos os objetos e referências
- um future divide o código e os dados referenciados por esse código em milhares de partes, que podem ser executadas a qualquer momento e em qualquer thread dependendo de condições conhecidas só depois do início da execução
- um future que lê dados do cliente só deveria ser executado quando houver dados para ler naquele socket, mas anotações de lifetime não informam esse momento
- O Rust não embute um runtime de futures na linguagem e deixa isso para bibliotecas como Tokio
- o usuário ganha liberdade para escolher alternativas adequadas ao seu ambiente
- ainda assim, mesmo imaginando um mundo em que Tokio fosse embutido na linguagem, as mesmas regras se aplicariam, então esse ponto é um detalhe secundário neste argumento
A pressão criada por Send, 'static e Arc
- Para convencer o compilador, os dados precisam ser movidos com
Sendou passados por referências com lifetime'static - Em código
async, é comum que várias tasks compartilhem estado, então mover dados sem cópia muitas vezes não se encaixa - Referências também são difíceis, e não existe um equivalente a
thread::scopeque limite o lifetime de um future a algo menor que “para sempre” asyncé contagioso, então qualquer função que chame uma funçãoasynctambém precisa serasync- por isso, esses problemas de lifetime e mobilidade não precisam ser resolvidos só em algumas funções, mas repetidamente por todo o código
- dá para quebrar a cadeia esperando a conclusão de um future em runtime com
block_on, mas isso não é composicional e, se for aninhado, pode fazer o runtime entrar em panic
- Arc é a ferramenta para lidar com lifetimes dinâmicos entre várias threads, permitindo passar pelo borrow checker e compilar o código
- Mas o uso amplo de
Arctambém torna menos claros os lifetimes de objetos e recursos- não fica claro quando recursos como memória, arquivos e sockets serão liberados
- acabam surgindo perdas parecidas com as de GC sem ganhar as vantagens reais de um GC, como throughput de allocation, baixa fragmentação e evitar vazamentos por ciclos
Armadilhas adicionais do Rust async
- As coroutines do Rust são stackless, então o compilador transforma cada coroutine em uma máquina de estados que avança até os pontos de
.await- funções
asyncrecursivas se tornam tipos definidos recursivamente - quem quer simplesmente chamar a si mesmo precisa fazer boxing manual ou usar uma crate como async-recursion
- funções
- Um future não faz nada até ser aguardado com
await - Uma task começa a trabalhar em um pool de threads do runtime e retorna um future que sinaliza a conclusão
- Não existe nenhum mecanismo que impeça chamar código blocking dentro de um future
- nem que impeça que essa chamada bloqueie a thread do runtime em que ele estiver rodando
- isso entra em conflito com o objetivo central de usar
async
A diferença entre Rust comum, Haskell e Go
- O Rust
asynctem um sabor bem diferente do Rust “normal”- há mais armadilhas
- é mais difícil de entender e de ensinar
- O usuário acaba entre duas opções
- entender profundamente como a abstração realmente funciona e escrever código complexo
- espalhar elementos como
Arc,Pine'staticpelo código todo e torcer para dar certo
- Mesmo equipes experientes podem querer usar Rust em um projeto novo e acabar travadas nesses detalhes
- Em Haskell e Go, “código async” é código comum
- as duas linguagens escondem a diferença entre código blocking e non-blocking atrás de um runtime robusto
- problemas de lifetime ficam a cargo do garbage collector
- Nesse tipo de software de espaço do usuário com concorrência em larga escala, a forma como o runtime e o GC escondem essas diferenças funciona como uma vantagem direta
- Talvez o Rust não seja uma boa ferramenta para software de espaço do usuário com concorrência em larga escala, e possa ser melhor usá-lo em projetos que não tenham esse tipo de exigência
1 comentários
Opiniões no Hacker News
Estou escrevendo um cliente de metaverso de alto desempenho em Rust, atualmente com cerca de 40 mil linhas
O vídeo de demonstração está em https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz
Um metaverso de verdade precisa processar conteúdo criado por usuários quase em tempo real; por isso exige 2 a 3 vezes mais VRAM do que jogos semelhantes, centenas de Mbps de largura de banda para carregar assets do servidor, vários CPUs e Vulkan para fazer renderização e upload para a GPU em paralelo
Isso não é uma concorrência em “escala web”, com pequenos servidores rodando separadamente no mesmo espaço de endereçamento, mas sim uma arquitetura em que trabalham juntos uma thread de renderização de alta prioridade, uma thread de atualização de eventos de rede, threads de carregamento e descompressão de assets e várias threads responsáveis por objetos em movimento, LOD, limpeza de cache etc.
Em Rust, uso bastante locking sem estado global além de constantes, uso canais onde faz sentido, e a árvore principal de objetos fica sob propriedade única e é tratada principalmente pela thread de atualização. As conexões de objetos gráficos são gerenciadas com contagem de referências por
Arc, e meshes e texturas são enviados para a GPU via Rend3/WGPU/VulkanSe eu tivesse feito isso em C++, estaria lutando contra crashes o tempo todo; em Rust, crashes relacionados a memória acontecem mais ou menos uma vez por ano, e em geral eram culpa de código
unsafede terceiros. No meu código,unsafeé proibido; compilar é difícil, mas, quando compila, tende a “simplesmente funcionar”, o que considero muito melhor do que depurar concorrênciaTambém tenho reclamações. Rust é forte contra data races, mas não impede deadlocks, então seria necessário um analisador estático que rastreasse a ordem de locks ao longo dos caminhos de chamada.
asyncnão combina com tarefas centradas em computação e várias threads com prioridades diferentes, mas continua se infiltrando como dependência. Estruturas comuns com propriedade única e referências reversas ficam difíceis demais semRceWeak, e o sistema de traits também é complexo, gerando código duplicado nas partes de processamento de assets em que orientação a objetos seria naturalAs crates centrais de gráficos ainda não estão maduras. A frase “Rust tem 5 jogos e 50 motores de jogo” não é um problema da linguagem, mas do ecossistema; mesmo comparando com https://gamedev.rs/, ainda parece faltar desenvolvimento sério de jogos em Rust. Para desenvolvimento profissional de jogos com cronograma, o ecossistema de jogos em Rust ainda não está pronto; eu diria que precisa de algo como mais 1 ano de trabalho de umas 5 pessoas
asynctenham seus problemas, no geral as vantagens são muito maioresComo o
lockdepdo Linux, daria para analisar quais locks são adquiridos enquanto outro lock já está retido e avisar sobre combinações perigosas antes que o programa realmente trave. Em locks complexos, talvez fossem necessárias anotações como “esta classe de lock é sempre adquirida em ordem de endereço”, mas parece implementávelCondições de corrida ainda podem ocorrer fora do acesso a dados: https://news.ycombinator.com/item?id=23599598
Você pode criar vários pools de threads e rotear futures para o lugar adequado, ou escrever seu próprio event loop e consumir de várias filas de eventos com prioridades diferentes. A segunda abordagem, se o tempo de execução das tarefas for limitado, permite dar garantias soft real-time às tarefas de alta prioridade enquanto mantém o progresso das de baixa prioridade mesmo com a CPU a 100%
Sobre Rust
async, há um ponto curiosoSe você usa um monte de
Arc,RwLocke estado compartilhado, a coisa fica bagunçada, e é correta a crítica de que, especialmente quando'staticcomeça a se espalhar por todo lado, ele infecta tudo como funções coloridas. Antigamente, tentei simplesmente colocarArce lidar de forma esperta com empréstimos e lifetimes, e virou uma confusãoMas Rust também tem canais. O código que escrevo hoje, em sua maior parte, tem algumas tasks atendendo canais, olhando as mensagens recebidas e, quando necessário, colocando mensagens a serem enviadas a outras tasks nos canais apropriados. Não compartilho objetos. Se várias tasks precisam de um objeto grande, deixo-o dentro de uma task que responde a consultas relacionadas por mensagens, ou faço cada task criar sua própria cópia a partir do fluxo de mensagens
Mesmo assim, há textos demais sobre como usar
Arce como lidar com lifetimes. Isso pode ser necessário se você estiver implementando um runtimeasync, mas não entendo bem por que um usuário médio de biblioteca deveria se concentrar tanto nissoasyncnão significa necessariamente multithreading; se oasyncroda na mesma thread, não há compartilhamento, então também não é preciso colocar palavras-chave mágicas em tudo que será compartilhadoAo cruzar threads, em vez de manter um monte de estado compartilhado, envie sinais por canais. Se houver um estado global realmente necessário, crie uma pequena struct que encapsule mecanismos de acesso exclusivo como
Arc/RwLock, e faça com que, para o chamador, pareça uma simples chamada de funçãoTambém não entendo muito a preocupação com
Send+Sync. Pela minha experiência, a maioria das coisas é facilmenteSend+Sync, e as que não são ou não deveriam ser assim ou não poderiam ser. Às vezes quero escrever código sem pensar nos detalhes, mas, quando é preciso concorrência e paralelismo eficientes, microssegundos e throughput passam a importar; aí é preciso escrever código para o computador real corretamenteMas codar desse jeito é bem diferente do JavaScript
async, que parece código síncrono com green threads adicionadas. As pessoas tentam escrever código do jeito a que estão acostumadas, e acho que por isso acabam caindo no caminho deArceRwLockem RustEstruturar o problema como dados fluindo entre tarefas, conectadas por filas, evitando estado compartilhado, é uma forma melhor de lidar com multithreading em qualquer linguagem
asyncé, na prática, um Rust muito mais difícil, e é uma pena que tenha acabado sendo praticamente imposto a todos, embora os projetos que realmente precisam dele devam ser algo como 1%Ainda assim, nesse 1% ele é realmente excelente. Em serviços que têm como núcleo processar uma enorme quantidade de chamadas de rede, como linkerd ou nginx, em jogos que rodam um número imenso de tarefas leves, ou em sistemas embarcados que precisam de concorrência cooperativa, async Rust se torna uma arma poderosa
A maior parte do código em nível de sistemas e aplicações não precisa de E/S assíncrona. Para apps REST, um pool de threads basta; e, mesmo quando
asyncé necessário, o normal é limitá-lo a uma pequena parte, como rede, e conectar o restante com threads e canais em um modelo híbridoA comunidade Rust usou
asyncde forma indiscriminada demais em todo lugar, e o Rust com E/S bloqueante, que oferece uma experiência de usuário melhor, virou cidadão de segunda classe no ecossistema. Também há vários frameworks web assíncronos bem projetados, como Axum e Warp, mas no lado bloqueante as opções são muito mais limitadas, comotiny_http,rouilleeastraAo optar por corrotinas sem stack, surgiram
async/awaite o problema das funções coloridas, além do atrito mencionado no texto. Go não tem esse problema porque usa corrotinas com stackNo início, Rust também considerou corrotinas com stack, mas concluiu que isso exigiria um runtime preemptivo de corrotinas e teria custo alto, então seguiu com o modelo sem stack. Só que a maioria das pessoas não usa
asyncRust sem runtime; usa Tokio, e Tokio faz, na prática, quase tudo que o runtime que se queria evitar fariaAssim, muitos usuários de
asyncRust acabam ficando com o pior dos dois mundos. No mundo embarcado, há quem useasyncRust com um runtime bem fino, mas eles são poucos e nem mesmo todos estão totalmente convencidosreqwest, que trazh2, que por sua vez traztokioasyncquando a plataforma oferece threads virtuaisComo alguém que usa Java, estou tentando abandonar todo o paradigma assíncrono e reescrever o código em um modelo bloqueante sobre threads virtuais, em que bloquear é aceitável
asyncse espalhou por crates demais, então o programa inteiro acaba virandoasyncou, no mínimo, passando a depender de Tokio para muita coisaSe você quer um servidor web, parece ser
async + tokioou vá embora; e, se não quiser conectores SQL assíncronos, a sensação é que terá de escrever os seus próprios. Cada um resolve os problemas trazidos porasyncde um jeito diferente, e coisas como closuresasyncdão a impressão de abrir os portões do inferno dentro do compiladorÉ bom que o próprio Rust e o compilador ajudem a resolver problemas, mas um ecossistema que chega perto de “ou é
asyncou faça você mesmo” não é suficientefutures, muito sofrimento poderia ter sido reduzidoSeriam necessárias coisas como traits que os executores devem implementar, ou um executor bloqueante básico para executar código assíncrono a partir de código síncrono. Hoje, só criar uma biblioteca que suporte vários runtimes assíncronos já é um suplício; no fim, as pessoas acabam dando suporte apenas a Tokio ou, no máximo, adicionando
async-stdNão sou especialista em Rust
async, mas, depois de escrever alguns milhares de linhas de Rust síncrono neste mês, minha impressão é que, quando orustcdificulta alguma abordagem, geralmente há um bom motivo para isso e existe um caminho melhor para obter um resultado parecidoSe você está aprendendo a linguagem, eu recomendaria primeiro se acostumar com código síncrono comum, laços, condicionais e regras de empréstimo.
asyncainda está evoluindo muito, não só na implementação, mas também em um nível filosófico: “o que é o assíncrono e como ele deve aparecer para o usuário?”O compilador depende muito de traits, mas os recursos das traits para lidar com
asyncainda não foram estabilizados. Há, por exemplo, trabalhos como https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i...Se os recursos assíncronos em traits ainda não foram estabilizados, atacar o código assíncrono em Rust por ainda não ser bonito é parecido com criticar o rascunho inicial de um livro que um dia ficará pronto
Também fico pensando em como impedir que o assíncrono se espalhe por toda a base de código
A ideia atual é uma estrutura em que uma thread de E/S divide os eventos do sistema vindos de
liburingouepollem duas etapas, “submit” e “handle”, e os envia para outros componentes. Por exemplo, ao criar umatcp-connection, seria possível assinar eventos assíncronos como “pronto para escrita” e “pronto para leitura”; o evento de pronto para escrita retiraria dados de um buffer preenchido por um mutex comum e os enviaria viaEPOLLOUT/io_uring_prep_writevPara entregar eventos entre threads, poderia ser usado um ring buffer de múltiplos produtores e múltiplos consumidores no padrão LMAX Disruptor. Threads da aplicação ou pools de threads teriam seus próprios loops de eventos e processariam esse ring buffer
Também estou trabalhando em uma sintaxe para expressar a ordem de disparo de eventos assíncronos; ela se parece com um pipeline de Bash e eu a chamo de
statelines:initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3A vida útil de um
Arcnão é algo desconhecido; ela é determinada por onde e como ele é mantidoA dissonância deste texto parece vir do fato de o autor tentar aplicar à força ao Rust um modelo mental anterior, como o de garbage collection, em vez de aprender Rust e trabalhar de acordo com a linguagem. É uma armadilha comum ao aprender uma linguagem nova, mas Rust faz as pessoas tropeçarem nela com ainda mais frequência
Mas isso é praticamente o oposto do objetivo do borrow checker, que é restringir estaticamente a vida útil dos objetos em tempo de compilação
Na prática, foi quase o contrário. Depois de fazer programação de sistemas em C, C++ e Rust por cerca de 10 anos, passei a usar bastante Haskell no meu emprego atual, e foi bem revelador perceber que runtimes grandes de linguagem e garbage collection não são monstros em certos domínios de problema
asyncatrapalha otimizações que o compilador consegue fazer em código não assíncronoO trecho sobre brigar com
Weakparece indicar uma tentativa de criar uma estrutura de ownership complexa, algo que não é fácil em Rust de modo geral. Eu uso smart pointers fracos muito raramenteCanais quase não são mencionados, mas são a principal ferramenta para fazer partes diferentes do programa se comunicarem em código assíncrono ou na ponte entre código assíncrono e síncrono. Também há abstrações de sinalização como
Notifye semáforosMutexes são lentos e tendem a virar gargalos, e estado compartilhado rapidamente fica complexo. Isso é conhecido há muito tempo. O problema pode estar, desde o início, em uma estrutura como
BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORRORA observação de que não se consegue impedir chamadas a código bloqueante em um contexto assíncrono é válida, mas, se necessário, isso é relativamente gerenciável com algo como
tokio::spawn_blockingÉ provável que o autor saiba o que é
Arce como ele funciona; o ponto parece ser mais que, em Rustasync, acaba-se usandoArccom muito mais frequência no lugar de RAII comum do que em código síncronoSe 90% dos objetos do programa são contados por referência, talvez seja melhor usar garbage collection com rastreamento do que pagar o custo de muitas pequenas alocações/liberações no heap e operações atômicas. O exemplo do tutorial do Tokio aponta em uma direção parecida: https://tokio.rs/tokio/tutorial/shared-state
Fico curioso se um garbage collector de rastreamento real em Rust poderia tornar aplicações assíncronas comuns, como servidores HTTP, significativamente mais rápidas: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
Arcnão é aleatória; ela é estaticamente desconhecidaArcdo Rust pode ser movido ou emprestado, e também pode ser usado sem tocar no contador de referênciasEm muitos casos, ele é muito mais barato do que objetos em linguagens com contagem de referências implícita
Gosto de Rust, mas
asyncé uma bagunça, e não dá para escrever código assíncrono como se escreve código síncronoEstou cada vez mais convencido de que misturar os dois é uma má ideia, e talvez a abordagem ao estilo Go, mantendo tudo síncrono e oferecendo apenas uma primitiva de canal
async, seja a corretaNo momento estou encanando a lógica para chamar métodos síncronos a partir de uma struct que implementa
Future, e é um desafio bem interessante. Talvez dê para tornar abstrações assíncronas de custo zero relativamente fáceis para o usuário, mas a dor acaba ficando com o desenvolvedor de bibliotecasasyncdefinitivamente também é doloroso para usuários finais, e parece que se está usando uma linguagem separada sem recursos centrais do Rust, como lifetimes e tipos explícitos, tudo ainda salpicado dePinComo não dá para executar fibers com escopo, acaba-se colocando
Arcem tudo;Piné difícil de usar semunsafe; e uma mudança minúscula em uma função assíncrona pode tornar os futures de toda a base de código!Sendasync. Resta ver como isso vai evoluirAsync Everything é uma linguagem ruim
async/awaitfoi uma ideia terrível para corrigir o problema de o JavaScript não ter threads bloqueantes de verdade, e agora está sendo enxertado em todas as linguagens. Vai dividir a linguagem e o ecossistema de bibliotecas em dois e causar sofrimento por muito tempoQuem já fez multithreading fora do JavaScript sabe que atores ou processos sequenciais comunicantes são a melhor forma de lidar com multithreading
A tese de Joe Armstrong também explica que a única maneira de entender programas multithread é escrever código estritamente sequencial para cada thread e não misturar o código de várias threads em um só lugar. Para minimizar a lacuna conceitual, uma atividade concorrente real do problema deve corresponder exatamente a um processo concorrente da linguagem de programação: https://erlang.org/download/armstrong_thesis_2003.pdf
A crítica de Ron Pressler, que implementou o Project Loom do Java, a
async/awaittambém é boa: https://www.youtube.com/watch?v=oNnITaBseYQEle era bastante ambivalente em relação ao JavaScript em si; o objetivo principal era encontrar uma abstração para lidar com o loop de eventos de E/S do
epoll()sem dar vontade de enfiar os dedos nos olhos. Antes disso, ele tentou muitas outras abordagensasync/awaitna verdade começou em C#, não em JavaScriptAnders Hejlsberg, do C#, também criou TypeScript, e recursos do TypeScript como classes, arrow functions e
async/awaitacabaram entrando no ES6+Acho que foi uma ótima solução em JS/TS, com seu loop de eventos de thread única. Mas, quanto mais baixo nível é a linguagem, pior ela fica como abstração; por isso, a maior parte da crítica a
asyncRust feita aqui é válidaO texto explica bem a complexidade e a dificuldade do Rust
async, mas também é importante lembrar que uma das filosofias centrais do Rust é segurança de memória sem sacrificar desempenhoOs padrões assíncronos do Rust, especialmente a forma como fazem o compilador garantir a segurança dos dados, demonstram bem essa filosofia. Embora haja complexidade, há valor em um modelo de concorrência mais seguro que força o desenvolvedor a pensar profundamente sobre dados e fluxo de execução
Rust pode não ser a resposta para todas as grandes aplicações concorrentes em espaço de usuário, mas em sistemas nos quais robustez e segurança são a prioridade, os trade-offs podem se justificar. À medida que o ecossistema evoluir, é provável que surjam mais abstrações e bibliotecas que reduzam essas dores
Escrevo bastante Rust lock-free baseado em
async. O principal problema é que os futures do Tokio são'static, e isso vem de um erro de design profundamente enraizado no ecossistema Rust: a decisão de que vazamentos de memória são segurosPor causa disso, não é possível garantir estaticamente que um future será devidamente limpo. Quando uma tarefa assíncrona é criada, se alguém esquecer o future com
std::mem::forget, o borrow checker não consegue saber que as referências passadas transitivamente por esse future ainda estão vivasEm vez de espalhar
Arcpor todo lado, uso este crateunsafe: https://docs.rs/async-scoped/latest/async_scoped/Isso pega 99% dos bugs que eu teria criado em C++, então é um compromisso razoável. Também há trabalho em andamento para implementar futures não-
'staticde forma segura, e espero que dê certoOutro grande problema é que
async traithoje exige futures boxed, o que acrescentamalloc/freea cada fronteira de chamada de função, mas isso está no roadmap para ser corrigido este anoO conselho de “é só usar canais” também espalha o fluxo de controle por todo lado em bases de código grandes. Canais parecem uma versão moderna de
GOTO; eu também os uso, mas evito quando só preciso executar algumas coisas em paralelo e esperar sua conclusão'static, e sim que apenas futures'staticpodem serspawnados para aproveitar a concorrência do runtimePara ser
poll()ado, um future precisa estarPin, e umT: !UnpinemPinno fim precisa terDropchamado: https://doc.rust-lang.org/std/pin/#drop-guaranteeFutures gerados pelo recurso
asyncdo compilador têm essa propriedade, e futures manuais também podem incluirPhantomPinned. Graças a isso, depois que forampoll()ados, brincadeiras commem::forgetpodem ser consideradas comportamento indefinido, e bibliotecas de futures intrusivos e autorreferenciais se tornam possíveis: https://docs.rs/futures-intrusive/latest/futures_intrusive/Um future pode continuar vivo e vazar por causa de
Arc/Rc, mas, do ponto de vista de quem desenvolve bibliotecas, isso não é razoavelmente distinguível do uso normal, ou não é algo com que seja tão necessário se preocuparRc, ou então colocar fronteiras de traitsunsafeinfecciosas