Entendendo o `Pin` do Rust
(without.boats)- O
Pindo Rust é um elemento fundamental introduzido para lidar com segurança com estados autorreferenciais dentro de Futures gerados por async/await - Como Futures async armazenam estado em cada ponto de
await, campos dentro de um objeto podem referenciar outros campos do mesmo objeto, tornando-o um tipo autorreferencial - As propostas de move constructor, offset pointer e do design
?Movenão foram adotadas por causa de problemas de custo de rastreamento em runtime, viabilidade de compilação e compatibilidade retroativa com APIs existentes - O design final foi o
Pin, que envolve um ponteiro e coloca o alvo em um pinned typestate; graças ao auto traitUnpin, a maioria dos tipos continua podendo ser movida como antes - A dificuldade do
Pinvem menos da ideia de imobilidade em si e mais das limitações de um tipo de biblioteca; reborrowing,Pin::set, pinned projection e a interação comDropreduzem bastante a usabilidade
O problema que exige Pin
- No ecossistema async de Rust,
Pine pinning são fundamentos centrais, mas continuam sendo uma área difícil e cercada de mal-entendidos para quem aprende async Rust - O objetivo do
Pinnão é permitir que usuários criem diretamente tipos autorreferenciais usando apenas Rust seguro- Ele existe para manipular com segurança Futures autorreferenciais gerados pelo compilador em funções async, ou tipos autorreferenciais criados com código unsafe por runtimes como o tokio
- No exemplo de
async fn bar, no pontofoo(&mut z).await, é preciso armazenar juntos no mesmo estado do Future tantozquanto o FutureFooque referenciaz- Nesse caso, um campo dentro do objeto Future referencia outro campo do mesmo objeto
- Isso faz desse tipo de Future um tipo autorreferencial
- Se o objeto for movido depois de entrar nesse estado, as referências internas apontarão para a localização anterior de memória, que pode já não ser válida ou ter sido reutilizada por outro valor
- Antes do
Pin, em Rust, se você tinha ownership ou uma referência mutável, podia mover o objeto; por isso era necessário um modo de expressar que, depois de certo ponto, ele não poderia mais ser movido
Abordagens que não viraram solução
-
move constructor
- Um move constructor executaria código, como um destructor, quando um valor fosse movido, ajustando ponteiros autorreferenciais para a nova posição
- Em Rust, porém, os ponteiros não ficam apenas “dentro” do valor movido; por exemplo, eles também podem estar em um vetor de ponteiros que apontam para o próprio estado
- Rastrear todos esses ponteiros exigiria, no fim, um gerenciamento de memória em runtime parecido com coleta de lixo
- Rust decidiu cedo não ter move constructor, e muito código unsafe depende da premissa de que mover um valor pode ser feito só com cópia de memória
- Adicioná-lo depois seria uma breaking change
-
offset pointer
- Offset pointer compilaria uma autorreferência não como referência comum, mas como um deslocamento em relação ao endereço do próprio objeto autorreferencial
- Em tempo de compilação, nem sempre dá para determinar quais referências são autorreferenciais
- Dependendo do ramo, o mesmo valor pode apontar para dentro do próprio objeto ou para algo externo
- Para tratar isso, seria preciso compilar referências como algo parecido com um enum entre offset e reference, o que foi considerado impraticável na época do trabalho em async/await
Requisitos do pinned typestate
- Um Future autorreferencial não precisa ser sempre imóvel desde o início; durante parte de sua vida ele pode ser movido livremente, mas a partir de certo momento não pode mais
- Ele precisa poder ser movido enquanto é combinado com outros Futures
- Depois de ser colocado no local em que ficará enquanto for
pollado, não deve mais ser movido
- O modelo de Ralf Jung adiciona, além dos typestates existentes “owned” e “shared”, um terceiro estado para Futures autorreferenciais: o pinned typestate
- Quando um objeto entra no pinned typestate, ele nunca mais deve ser movido
- Mais precisamente, não se pode invalidar a memória daquele objeto sem antes executar seu destructor
- Na prática, isso equivale à exigência de não deslocar o objeto para outro endereço
- A maioria dos tipos não pode conter autorreferências, então o pinned typestate não tem significado especial para eles
- Para esses tipos, é desejável poder escapar das restrições de pinning e voltar a ser movidos
- O modelo formal detalhado do pinned typestate está descrito por Ralf Jung em A Formal Look at Pinning
Por que o design ?Move falhou
- Antes do
Pin, tentou-se um design baseado em um novo trait chamadoMove- A maioria dos tipos implementaria
Move - Tipos que podem conter autorreferências não implementariam
Move - Ao criar uma referência para um valor de um tipo que não implementa
Move, esse valor entraria em pinned typestate e não poderia mais ser movido
- A maioria dos tipos implementaria
- A abordagem parecia intuitiva porque ligava o momento de criar a referência à transição para o estado pinned, garantindo segurança
- Ela chegou a ser implementada em um branch do compilador
- A limitação fundamental era que às vezes você quer referenciar temporariamente um valor que poderá se tornar autorreferencial depois, mas ainda não quer fixá-lo com pinning
- Por exemplo, você pode querer guardar o valor temporariamente em um
Optione depois extraí-lo comOption::take
- Por exemplo, você pode querer guardar o valor temporariamente em um
- O problema maior era a compatibilidade retroativa
Movenão podia ser um auto trait- Isso porque já existiam APIs estáveis, como
mem::swap, que assumem que sempre é possível mover valores por meio de uma referência mutável
- A tentativa de adicionar isso como
?Movetambém não era retrocompatível por causa de associated types- O lugar onde se adiciona um bound
?Traita um associated type é a própria definição do trait - Relaxar o bound de associated types em traits já existentes pode quebrar código que dependia desse bound
- O future type associado de
IntoFuture, oTargetdeDerefMut, tipos de retorno de função, itens de iterator, retornos do operador de índice e dos operadores aritméticos, entre muitas outras operações básicas, estão todos ligados a associated types
- O lugar onde se adiciona um bound
- Nem uma edition resolveria isso facilmente
- Crates de editions diferentes precisam continuar combinando entre si com a mesma interface de traits
O design de Pin
- O design final representa o pinned typestate não como uma propriedade do tipo do objeto, mas como um estado criado por um ponteiro especial
Piné um wrapper type que envolve um ponteiro- Ele pode envolver até tipos de referência embutidos na linguagem
- Também pode envolver smart pointers definidos em biblioteca, como
Box
Pincoloca em pinned typestate o alvo apontado por esse ponteiro, e esse alvo não pode mais ser movido- Para minimizar mudanças, esse design foi implementado como uma API de biblioteca, e não como funcionalidade do compilador
- Código que realmente precisa modificar um objeto pinned deve acessar APIs unsafe
- Nisso, é preciso garantir que o objeto não seja movido por meio de uma referência mutável comum
- Como para a maioria dos tipos não há diferença prática entre estado pinned e estado normal, foi adicionado o auto trait
Unpin- Se um tipo não pode ser autorreferencial, é possível obter de um pinned pointer uma referência mutável sem usar unsafe
- Objetos que implementam
Unpincontinuam sendo seguros mesmo se movidos para fora de umPin
- Como o pinning se aplica apenas a pinned pointers, referências comuns sem pinning continuam funcionando também com tipos que não são
Unpin - Mais explicações estão na documentação padrão do tipo
Pine do módulopin - A maior vantagem desse design foi poder ser adicionado sem quebrar código existente
- APIs como
swap, que podem mover dados referenciados, exigem uma referência mutável - Quando um objeto é fixado com
Pin, essas APIs deixam de poder ser chamadas sobre ele - Como o pinned typestate se aplica só a referências pinned especiais, ele não quebra as garantias gerais de compatibilidade retroativa da linguagem Rust
- APIs como
Problemas de usabilidade de Pin
- O
Pinatendeu aos requisitos de forma retrocompatível, mas, quando o usuário precisa lidar com ele diretamente, surge um abismo de complexidade - Uma explicação é que modificar um objeto pinned exige código unsafe
- Mas esse problema não deve ser exagerado
- É possível atribuir com segurança a um objeto pinned usando
Pin::set - Na prática, o código que realmente precisa modificar objetos pinned costuma ser o código gerado pelo compilador ao reduzir funções async a Futures; usuários raramente escrevem isso manualmente
- Outra explicação diz que
Piné difícil porque funciona de forma condicional, mas essa não é a causa central- Rust já tem recursos que mudam de comportamento conforme a condição e ainda assim permanecem compreensíveis
- Non-lexical lifetimes são um exemplo: fazem o lifetime terminar em pontos diferentes dependendo do ramo de execução
- O problema central é que
Piné um tipo de biblioteca puro, enquanto tipos de referência comuns são embutidos na linguagem e recebem bastante suporte sintático e sugar- Coisas que funcionam naturalmente com referências comuns desaparecem com referências pinned
- O modelo mental que o usuário formou com base no comportamento das referências aceitas pelo compilador se quebra ao usar referências pinned
reborrowing e Pin::as_mut
- Uma referência mutável comum
&mut Tnão implementaCopy, mas ainda assim pode ser passada várias vezes como argumento- Isso acontece porque o compilador faz reborrowing implicitamente, como se colocasse
&mut *xno lugar dex
- Isso acontece porque o compilador faz reborrowing implicitamente, como se colocasse
Pin<&mut T>, por ser um tipo comum de biblioteca e não implementarCopy, não tem essa conveniência- Se você usar
Pin<&mut T>mais de uma vez, pode receber erro de uso após move, ou até erros de lifetime mais difíceis de entender - É preciso chamar explicitamente
Pin::as_mutpara fazer reborrow
- Se você usar
- Com referências mutáveis comuns, também é possível atribuir diretamente com dereference e o operador de atribuição, mas em
Piné preciso aprender o métodoset- Esse aumento de APIs especiais acontece porque
Piné um tipo de biblioteca sem suporte sintático da linguagem
- Esse aumento de APIs especiais acontece porque
pinned projection e Drop
- Pinned projection é o problema de obter, a partir de uma referência pinned para um objeto, uma referência pinned para um campo desse objeto
- Projection significa acessar um campo a partir do objeto
- Como isso é muito mais difícil do que acesso a campo em referências comuns, usam-se crates de terceiros como pin-project-lite
- Esses crates exigem aprender novas APIs complexas, incluindo macros
- A pior interação acontece entre pinned projection e o trait
DropDrop::droprecebe uma referência mutável comum- Se um tipo tiver um campo autorreferencial, esse campo for pin-projectado e
pollado, e depois o destructor mover esse campo, a garantia de pinning pode ser quebrada - Por exemplo, se dentro do destructor esse future for fixado na stack e
pollado, a garantia de pinning anterior é violada
- Crates como
pin-project-litetratam esse problema restringindo a capacidade de definir destructors- Na prática isso funciona, mas adiciona complexidade que precisa ser documentada ao explicar as garantias de pinning
Dropfoi estabilizado antes dePin, então foi necessário contornar a situação
Avaliação atual e próximo rumo de melhoria
Pinpermitiu compilar funções async com referências arbitrárias em objetos autorreferenciais seguros- Referências são parte essencial da maneira como usuários de Rust escrevem código; sem isso, a usabilidade de async/await seria muito pior
- Ao mesmo tempo,
Pinfoi adicionado de forma totalmente compatível com versões anteriores de Rust Pinse tornou um componente fundamental do ecossistema que sustenta serviços de rede de alto desempenho e outros casos de uso de programação assíncrona- Mas lidar com referências pinned é muito mais difícil do que lidar com referências comuns, e
Pinrealmente cria um abismo de complexidade - O conceito central para a próxima direção de melhoria é o de pinned places
1 comentários
Opiniões no Hacker News
Sempre achei que Pin fosse difícil de entender porque não é explicado com clareza na documentação oficial
Em especial, há muitas explicações como “Pin garante que um objeto nunca será movido”, mas isso não é verdade
Isso só é correto quando o objeto não é
Unpin; a maioria dos objetos comuns éUnpin, então Pin normalmente não faz nadaLevei muito tempo para entender isso, e acho que o conjunto de tipos
Tem que Pin de fato tem significado é bem específico e estranho, e a documentação não enfatiza isso o suficienteClaro que os tipos que você de fato vai tratar como fixados, futures e streams, têm uma chance muito maior de ser esse tipo especial de objeto
Ainda assim, acho que a documentação melhorou bastante ao longo dos anos
Quando conferi ao escrever este texto, fiquei surpreso por ela estar focada em pontos bastante adequados; lembro que por volta de 2019 ela pendia muito mais para uma especificação de contrato que pareceria caber na referência do Rust do que na documentação da API de
stdAcho que o motivo de os usuários acharem Pin difícil é que Pin, por si só, não tem significado
Isso é diferente de outros wrappers da linguagem; uma exceção talvez seja
AssertUnwindSafe, que quase ninguém usa para seu propósito originalQuando se tem
Pin<&mut InnerType>, nada em Pin, seja na linguagem ou na biblioteca padrão, diz o que pode ou não pode ser feitoSó que, se
InnerTypedeclarar que éUnpin, isso significa que tudo o que se pode fazer com um ponteiro comum também pode ser feitoEm vez disso, Pin funciona no estilo “traga seu próprio significado”: o fornecedor de
InnerTypecria métodos e APIs adicionais, internamenteunsafe, para manipular objetos fixados com segurançaO propósito do Pin em si é oferecer um ponteiro com menos capacidades inerentes, como substituir via
&mutou retirar de umBoxe mover, para permitir que o tipo interno autorize com segurança capacidades adicionais em cima dissoAcho que essa ambiguidade de significado é o que mais confunde as pessoas, e eu também levei bastante tempo para entender
Os conceitos de campos estruturais e não estruturais são apenas um mecanismo para permitir padrões comuns de acesso, como “este campo é dado comum, mas aquele campo contém um objeto que quer ser fixado por conta própria”
Unpin, o alvo desse ponteiro nunca mais poderá ser movidoMais precisamente, significa que não se pode invalidar o alvo sem executar seu destrutor, e é por isso que mover é problemático
Ao abrir mão de certos direitos, você ganha outros, como o direito de armazenar valores autorreferenciais
Contratos entre componentes geralmente funcionam assim
Da mesma forma, se você abre mão do direito de modificar por meio de uma referência, pode ao mesmo tempo criar aliases dessa referência
Sempre que penso nisso, lembro de uma fala do filme Lincoln — embora seja um tema totalmente diferente e muito mais pesado: “Se obedecermos à lei, Alex, se obedecermos até o ponto de perder liberdades — por exemplo, a liberdade de oprimir — talvez descubramos outras liberdades que antes não conhecíamos”
No entanto, concordo que há um problema pedagógico no fato de que, em código seguro, não se pode usar diretamente esses direitos
Porque é difícil mostrar facilmente o que se pode fazer com uma referência fixada, além de “chamar o método
pollque o compilador criou”Desenvolvo profissionalmente em Rust há alguns anos, mas, sinceramente, não entendo Pin tão bem assim
Entendo a teoria, mas não tenho muita intuição sobre quando devo usá-lo
O uso de Pin na prática é quase “tentei fazer algo, o compilador reclamou, fixei uma coisa aqui e outra ali, e compilou”
No código do dia a dia, isso ainda não se tornou um obstáculo que eu realmente precisasse sentar e entender a fundo
unsafee agradecer às pessoas inteligentes do compilador por já terem resolvido tudo”Já em C++, eu costumava andar com frequência à beira do pântano das “coisas que não entendo, mas preciso usar”, e acabava sendo devorado por jacarés
Ao ensinar, para deixar claro que itens
Unpinnão são afetados porPin, seria bom usar analogias do mundo real em que uma ferramenta feita para manter algo no lugar não tem efeitoGanchos de velcro não prendem em superfícies lisas:
Pin→ velcro,Unpin→ superfície lisaÍmãs não afetam materiais não magnéticos:
Pin→ ímã,Unpin→ não magnético/vidro/latãoCola não adere a superfícies antiaderentes:
Pin→ cola,Unpin→ antiaderenteAssim fica claro que o “velcro” fixa algo no lugar, mas, se o objeto for “liso”, ele não é afetado pelo mecanismo do velcro
Considerando o estilo de nomenclatura do ecossistema Rust, teria sido bonito se os nomes das traits tivessem sido trocados para algo como ímã e não magnético
Acho que
Unpinsignifica que o objeto está pronto para ser fixado a qualquer momentoLi o texto ontem à noite, mas já esqueci se a fixação exige uma etapa de ajuste
Então vejo
T: Pin + !Unpincomo algo parecido com papel, que só pode ser fixado com grampos, enquantoT: Pin + Unpinse aproxima mais de um quadro com argola, que pode ser pendurado em um prego e depois retirado sem estragar a argolaO termo “identidade de valor” não é definido em nenhum lugar deste texto e também não consegui encontrá-lo na documentação do Mojo, então não fica claro em que a Modular se baseia para dizer que o Mojo resolve o problema que o Pin tenta resolver
Eu também não estou afirmando saber a resposta, mas isso me lembra uma excelente palestra de Dave Abrahams, que trabalhou com Chris Lattner na semântica de valores do Swift
O título da palestra é “Value Semantics: Safety, Independence, Projection, & Future of Programming”
[0] https://www.youtube.com/watch?v=QthAU-t3PQ4
Rust também tem referências como tipos de primeira classe, enquanto o Swift e, pelo que vejo, o Mojo permitem referências apenas como forma de passagem de parâmetros
O Mojo parece ter estendido os parâmetros
inoutdo Swift para também incluir uma forma de passagem por referência imutávelImpedir que referências sejam armazenadas dentro de objetos de fato resolve o problema de “structs autorreferenciais”, porque não seria possível implementar o tipo de código que Rust compila
Mas o parágrafo citado não parece estar falando nada disso sobre Mojo, então o significado fica bem confuso
Para mim, o problema é que, quando há uma referência
&mutpara algum valor, esse valor pode ser movido com coisas comomem::swap/replaceMas, na prática, é raro precisar fazer isso
Se isso não fosse permitido, parece que seria totalmente seguro ter uma referência
&mutpara valores autorreferenciaisTalvez pudesse haver uma forma de optar explicitamente por mover através de uma referência apenas quando necessário, e talvez tornar
swapereplaceunsafetivesse evitado todo esse problemaGostaria que alguém explorasse esse espaço de design
&mutcomo poderoso demaisSe
&mutnão concedesse a autoridade para mover o valor dentro dele, todo o design teria sido muito mais simplesPretendo tratar disso no próximo texto
Rust precisa manter compatibilidade retroativa e já decidiu que é possível mover valores a partir de
&mut, mas, sem ficar preso a decisões passadas, claramente seria possível um design muito mais limpomem::swapé apenas uma das formas de mover valores por meio de uma referência mutável, e há muitas outrasOption::takeé um exemplo que uso com bastante frequência, e seria muito estranho se isso fosseunsafeGosto de ver esse contexto. WithoutBoats já participou de muitas discussões bastante oportunas sobre iteradores assíncronos,
pollepinhttps://news.ycombinator.com/from?site=without.boats
Acho que não há muitas comunidades que mergulham publicamente tão fundo nos detalhes internos de uma linguagem, e é bem divertido acompanhar
O assíncrono ainda está meio cru e é muito complexo
Digo isso como alguém que escreveu código Rust 40 horas por semana nos últimos 3 anos
Dá para imaginar uma linguagem parecida com Rust que tenha construtores de movimento, em que todos os subtipos de
Futurecriados sejam opacos e alocados automaticamente no heapNesse caso, o usuário não teria como destruí-los e, por serem opacos e estarem em outro lugar no heap, também não haveria como movê-los, então Pin poderia deixar de ser necessário
Ter um construtor de movimento significa que, conceitualmente, mover é destruir e recriar
Isso tem o bom efeito de permitir mesclar e inlinear
Futures antes da execuçãoÉ parecido também com a imutabilidade de Rust. Não existe memória imutável, apenas referências imutáveis
Mas então cada chamada de função assíncrona teria uma alocação separada, o que é muito ruim para a localidade de memória
Alguma forma de pilha virtual seria muito melhor do que isso, mas, para otimizar a pilha para ser pequena por padrão, no fim seria necessário garbage collection
stdum traitMove, embutido na linguagem em um nível parecido comCopyMovedefiniria uma função que move um valor de um endereço de memória para outro, e structs semimpl Movenão poderiam ser movidasQuase todos os tipos teriam
#[derive(Move)], que bastaria para implementar uma função simples de movimento que copia os bytesMas isso abriria caminho para tipos autorreferenciais, futures e muitas outras coisas que precisam de comportamentos de movimento mais complexos
Na prática, talvez fizesse mais sentido dividir isso em dois traits, refletindo a diferença entre
CopyeCloneUm seria um marker trait para dizer ao compilador que os bytes podem simplesmente ser movidos, e o outro permitiria implementações personalizadas de “construtor de movimento”
Pin é difícil demais de entender, então eu gostaria que existisse Move
Um conceito complicado fica envolto em dupla negação e, às vezes, tripla negação. Quando vejo algo como
fn(...), penso “o que é isso?”, e quando chega a projeção de pinunsafe, eu já desistoNão sei quando é seguro e quando não é, e acabo simplesmente tirando as mãos disso
Migrar de um Rust sem
Movepara um Rust comMoveseria incômodoSeria preciso adicionar
#[derive(Move)]a quase todas as structs já escritas, e o mesmo valeria para astdNas edições existentes, para todos os tipos que não estejam fixados, o compilador teria de inferir implementações do trait
MoveMecanicamente seria possível, só daria muito trabalho
Rust assíncrono é horrível. Especialmente quando comparado aos futures/promises de quase todas as outras linguagens
Um dia alguém vai melhorar o modelo de segurança de memória de Rust e criar uma nova linguagem de sistemas parecida com Rust, com um trait
Movee futures melhoresPessoalmente, eu também gostaria que houvesse execução em tempo de compilação em vez do sistema de macros de Rust
Gosto de Rust e gosto de todo o trabalho que a equipe fez ao longo de anos
Mas a linguagem que eu realmente espero é a que virá depois de Rust
A mesma ideia, mas uma linguagem que aprendeu com os erros de Rust, e está ficando cada vez mais claro como seria essa linguagem melhor no estilo de Rust
Mal posso esperar
Mais um ótimo texto do WithoutBoats
Sinceramente, esse é um daqueles pontos em Rust em que fico aliviado por estar abstraído e enterrado dentro dos runtimes assíncronos
Ainda assim, tenho curiosidade sobre onde Pin é realmente usado, além de implementações customizadas de
FuturePela forma como é descrito, parece que talvez possa ser usado dentro de FFI
Por exemplo, se uma função
externretorna um*mut Te recebe um ponteiro, parece que envolvê-lo emPin<&mut T>poderia dar uma semântica melhorMas o texto diz: “outro fato sobre o estado fixado de um tipo é que ele é completamente irrelevante para a maioria dos tipos. Se o valor de um tipo nunca pode conter uma autorreferência, fixá-lo é inútil”
Ainda sou muito iniciante em FFI, então quero entender qual é a melhor forma de envolvê-lo em Rust seguro
Isso também se aplica ao interagir com tipos de sistema que dependem de endereço
Por exemplo, em mutex/futex de alguns sistemas operacionais, a documentação do kernel diz que o objeto de lock no espaço do usuário não pode mudar de endereço depois de inicializado; pelo que sei, a
stdusa algo equivalente a Pin para issoO detalhe peculiar é que o endereço não pode mudar mesmo quando não está bloqueado
Normalmente, a condição vale apenas enquanto está bloqueado e, nesse caso, como não é possível mover o alvo de uma referência ativa, Pin não é necessário
Parece que estão fazendo um trabalho enorme para não corrigir o verdadeiro problema: a ineficiência das threads
Todo código assíncrono, sem exceção, é um hack que implementa threads leves com muito açúcar sintático para gerenciamento de estado
Em linguagens como Rust, isso adiciona uma quantidade enorme de complexidade que originalmente nem precisaria existir
Se corrigíssemos os problemas de eficiência e escalabilidade das threads, tudo isso desapareceria
Sumiria num passe de mágica
É parecido com o
nullem linguagens como Java ter sido um “erro de custo na casa dos trilhões”Uma única decisão de design — ou, neste caso, a ausência de design — gera uma complexidade enorme
Cancelamento é muito útil em aplicações de rede e GUIs
Threads dificultam aproveitar bem tanto a CPU quanto a rede sem ocupar demais nenhuma das duas
Quando você começa a passar trabalho entre pools de threads, está entrando no caminho de reimplementar futures
Ou então acaba trabalhando com callbacks/eventos, o que fragmenta o código e é justamente aquilo para o qual
async/awaittentava ser o açúcar sintáticoA alternativa para cancelamento e timeouts é, como em Go, entrelaçar objetos
Contextpor todo o código, mas aí surge o problema de código nas pontas chamar ingenuamente funções que não respeitam corretamente oContextIsso é só um pouquinho melhor do que o problema de funções não assíncronas dentro de código assíncrono
Então fico curioso sobre o que, concretamente, deveria ter sido feito
Era para simplesmente levantar as mãos e dizer: “algum dia alguém talvez corrija o Linux e torne threads magicamente rápidas, então não vamos adicionar async à nossa linguagem”?
Além disso, se fizermos do sistema operacional o escalonador de todas as tarefas assíncronas, todos os runtimes terão de usar o escalonador do sistema operacional, o que inviabiliza diferentes projetos de escalonador
Mesmo depois de ficar claro que a “abordagem preferida” não é viável, parece que não há uma reavaliação de custo/benefício
“Quero o recurso X, não me importo com as consequências” raramente é uma jogada vencedora em design de linguagens