Rust: [Estabilização] assíncrono / aguardar MVP

Criado em 26 jun. 2019  ·  58Comentários  ·  Fonte: rust-lang/rust

Meta de estabilização: 1.38.0 (corte beta 15/08/2019)

Sumário executivo

Esta é uma proposta para estabilizar um recurso assíncrono / espera mínimo viável, que inclui:

  • async anotações em funções e blocos, fazendo com que sejam atrasados ​​na avaliação e, em vez disso, avaliem para um futuro.
  • Um operador await , válido apenas dentro de um contexto async , que leva um futuro como argumento e faz com que o futuro externo em que está dentro ceda o controle até que o futuro esperado seja concluído.

Discussões anteriores relacionadas

RFCs:

Problemas de rastreamento:

Estabilizações:

Principais decisões alcançadas

  • O futuro que uma expressão assíncrona avalia é construído a partir de seu estado inicial, sem executar nenhum código do corpo antes de render.
  • A sintaxe para funções assíncronas usa o tipo de retorno "interno" (o tipo que corresponde à expressão return interna) em vez do tipo de retorno "externo" (o tipo futuro que uma chamada para a função avalia)
  • A sintaxe para o operador await é a "sintaxe de ponto postfix," expression.await , em oposição à mais comum await expression ou outra sintaxe alternativa.

Trabalho de implementação bloqueando estabilização

  • [x] fns assíncronos deve ser capaz de aceitar vários tempos de vida # 56238
  • [x] tamanho dos geradores não deve crescer exponencialmente # 52924
  • [] Documentação mínima viável para o recurso assíncrono / espera
  • [] Testes de compilação suficientes do comportamento

Trabalho futuro

  • Assíncrono / espera em contextos sem padrão: async e await atualmente dependem do TLS para funcionar. Esse é um problema de implementação que não faz parte do design e, embora não esteja bloqueando a estabilização, deve ser resolvido eventualmente.
  • Funções assíncronas de ordem superior: async como um modificador para literais de fechamento não está estabilizado aqui. É necessário mais trabalho de design em relação à captura e abstração de fechamentos assíncronos com vidas úteis.
  • Métodos de característica assíncronos: Isso envolve um trabalho significativo de design e implementação, mas é um recurso altamente desejável.
  • Processamento de fluxo: O par para o traço Futuro na biblioteca de futuros é o traço Fluxo, um iterador assíncrono. Integrar o suporte à manipulação de streams em std e na linguagem é um recurso desejável de longo prazo.
  • Otimizando as representações dos geradores: Mais trabalho pode ser feito para otimizar a representação dos geradores para torná-los mais perfeitamente dimensionados. Asseguramos que isso seja estritamente um problema de otimização e não seja semanticamente significativo.

Fundo

Lidar com IO sem bloqueio é muito importante para o desenvolvimento de serviços de rede de alto desempenho, um caso de uso alvo para o Rust com interesse significativo de usuários de produção. Por esse motivo, uma solução para tornar ergonômico e viável escrever serviços usando IO sem bloqueio há muito é um objetivo do Rust. O recurso async / await é o culminar desse esforço.

Antes do 1.0, o Rust tinha um sistema greenthreading, no qual ele fornecia uma alternativa, uma primitiva de threading de nível de linguagem construída sobre IO sem bloqueio. No entanto, este sistema causou vários problemas: o mais importante é a introdução de um runtime de linguagem que impactou o desempenho mesmo de programas que não o utilizavam, aumentando significativamente a sobrecarga do FFI e tendo vários problemas de design não resolvidos importantes relacionados à implementação de pilhas de greenthread .

Após a remoção de greenthreads, os membros do projeto Rust começaram a trabalhar em uma solução alternativa baseada na abstração de futuros. Às vezes também chamados de promessas, os futuros tiveram muito sucesso em outras linguagens como uma abstração baseada em biblioteca para IO não bloqueante, e era sabido que, a longo prazo, eles se mapeavam bem para uma sintaxe assíncrona / espera que poderia torná-los apenas um pouco menos convenientes do que um sistema greenthreading completamente invisível.

O principal avanço no desenvolvimento da abstração Future foi a introdução de um modelo baseado em pesquisas para futuros. Enquanto outras linguagens usam um modelo baseado em retorno de chamada, em que o próprio futuro é responsável por agendar o retorno de chamada para ser executado quando for concluído, o Rust usa um modelo baseado em pesquisa, em que um executor é responsável por pesquisar o futuro até a conclusão, e futuro apenas informando ao executor que está pronto para fazer mais progresso usando a abstração Waker. Este modelo funcionou bem por vários motivos:

  • Ele permitiu que o rustc compilasse futuros para máquinas de estados que tivessem o mínimo de sobrecarga de memória, tanto em termos de tamanho quanto de direção indireta. Isso tem benefícios de desempenho significativos em relação à abordagem baseada em retorno de chamada.
  • Ele permite que componentes como o executor e o reator existam como APIs de biblioteca, em vez de uma parte do tempo de execução da linguagem. Isso evita a introdução de custos globais que afetam os usuários que não estão usando esse recurso e permite que os usuários substituam componentes individuais de seu sistema de tempo de execução facilmente, em vez de exigir que tomemos uma decisão de caixa preta para eles no nível do idioma.
  • Ele cria todas as bibliotecas de primitivas de simultaneidade também, em vez de associar a simultaneidade à linguagem por meio da semântica dos operadores assíncronos e de espera. Isso torna a simultaneidade mais clara e mais visível por meio do texto de origem, que deve usar uma primitiva de simultaneidade identificável para introduzir a simultaneidade.
  • Ele permite o cancelamento sem despesas gerais, permitindo que os futuros em execução sejam descartados antes de serem concluídos. Tornar todos os futuros canceláveis ​​gratuitamente tem benefícios de desempenho e clareza de código para executores e primitivos de simultaneidade.

(Os dois últimos pontos também foram identificados como uma fonte de confusão para usuários vindos de outras línguas nas quais não são verdadeiros, e trazendo expectativas dessas línguas com eles. No entanto, essas propriedades são ambas propriedades inevitáveis ​​do modelo baseado em pesquisas que tem outras vantagens claras e são, em nossa opinião, propriedades benéficas, uma vez que os usuários as entendam.)

No entanto, o modelo baseado em pesquisas sofreu de sérios problemas ergonômicos ao interagir com as referências; essencialmente, as referências entre os pontos de rendimento introduziram erros de compilação insolúveis, embora devessem ser seguras. Isso resultou em um código complexo e barulhento, cheio de arcos, mutexes e fechamentos de movimento, nenhum dos quais era estritamente necessário. Mesmo deixando esse problema de lado, sem o primitivo de nível de linguagem, o futuro sofria ao forçar os usuários a um estilo de escrever callbacks altamente aninhados.

Por esse motivo, buscamos o açúcar sintático assíncrono / aguardar com suporte para o uso normal de referências em pontos de rendimento. Depois de introduzir a abstração Pin que tornava as referências entre pontos de rendimento seguras para suporte, desenvolvemos uma sintaxe nativa async / await que compila funções em nossos futuros baseados em pesquisas, permitindo que os usuários obtenham as vantagens de desempenho do IO assíncrono com futuros ao escrever código que é muito semelhante ao código imperativo padrão. Essa característica final é o assunto deste relatório de estabilização.

descrição de recurso assíncrono / aguardar

O modificador async

A palavra-chave async pode ser aplicada em dois lugares:

  • Antes de uma expressão de bloco.
  • Antes de uma função livre ou uma função associada em um impl.

_ (Outros locais para funções assíncronas - literais de fechamento e métodos de característica, por exemplo, serão desenvolvidos posteriormente e estabilizados no futuro.) _

O modificador assíncrono ajusta o item que modifica "transformando-o em um futuro". No caso de um bloco, o bloco é avaliado para um futuro de seu resultado, ao invés de seu resultado. No caso de uma função, as chamadas para essa função retornam um futuro de seu valor de retorno, em vez de seu valor de retorno. O código dentro de um item modificado por um modificador assíncrono é referido como estando em um contexto assíncrono.

O modificador assíncrono realiza essa modificação fazendo com que o item seja avaliado como um construtor puro de um futuro, tomando argumentos e capturas como campos do futuro. Cada ponto de espera é tratado como uma variante separada dessa máquina de estado, e o método de "pesquisa" do futuro avança o futuro por meio desses estados com base em uma transformação do código que o usuário escreveu, até que finalmente alcance seu estado final.

O modificador async move

Semelhante aos fechamentos, os blocos assíncronos podem capturar variáveis ​​no escopo circundante para o estado do futuro. Como fechamentos, essas variáveis ​​são, por padrão, capturadas por referência. No entanto, eles podem ser capturados por valor, usando o modificador move (assim como fechamentos). async vem antes de move , tornando esses blocos async move { } .

O operador await

Em um contexto assíncrono, uma nova expressão pode ser formada combinando uma expressão com o operador await , usando esta sintaxe:

expression.await

O operador await só pode ser usado dentro de um contexto assíncrono, e o tipo de expressão ao qual ele é aplicado deve implementar o traço Future . A expressão await avalia o valor de saída do futuro ao qual é aplicada.

O operador await cede o controle do futuro que o contexto assíncrono avalia até que o futuro ao qual ele é aplicado seja concluído. Esta operação de ceder o controle não pode ser escrita na sintaxe de superfície, mas se pudesse (usando a sintaxe YIELD_CONTROL! neste exemplo), o desenho de await seria mais ou menos assim:

loop {
    match $future.poll(&waker) {
        Poll::Ready(value)  => break value,
        Poll::Pending       => YIELD_CONTROL!,
    }
}

Isso permite que você espere que os futuros concluam a avaliação em um contexto assíncrono, encaminhando a cessão de controle por meio de Poll::Pending para o contexto assíncrono mais externo, em última análise, para o executor no qual o futuro foi gerado.

Principais pontos de decisão

Rendendo imediatamente

Nossas funções e blocos assíncronos "rendem imediatamente" - construí-los é uma função pura que os coloca em um estado inicial antes de executar o código no corpo do contexto assíncrono. Nenhum código do corpo é executado até que você comece a pesquisar esse futuro.

Isso é diferente de muitas outras linguagens, nas quais as chamadas para um acionador de função assíncrona funcionam para começar imediatamente. Nessas outras linguagens, async é uma construção inerentemente concorrente: quando você chama uma função assíncrona, ela dispara outra tarefa para começar a executar simultaneamente com sua tarefa atual. No Rust, no entanto, os futuros não são inerentemente executados de forma concorrente.

Poderíamos ter itens assíncronos executados até o primeiro ponto de espera quando são construídos, em vez de torná-los puros. No entanto, decidimos que isso era mais confuso: se o código é executado durante a construção do futuro ou durante a votação, isso dependeria da colocação do primeiro await no corpo. É mais simples raciocinar para que todo o código seja executado durante a votação, e nunca durante a construção.

Referência:

Sintaxe de tipo de retorno

A sintaxe de nossas funções assíncronas usa o tipo de retorno "interno", em vez do tipo de retorno "externo". Ou seja, eles dizem que retornam o tipo que eventualmente avaliam, em vez de dizer que retornam um futuro desse tipo.

Em um nível, esta é uma decisão sobre o tipo de clareza preferido: como a assinatura também inclui a anotação async , o fato de eles retornarem um futuro é explicitado na assinatura. No entanto, pode ser útil para os usuários ver que a função retorna um futuro sem ter que notar a palavra-chave async também. Mas isso também parece um clichê, já que a informação é transmitida também pela palavra-chave async .

O que realmente desequilibrou a balança para nós foi a questão da elisão vitalícia. O tipo de retorno "externo" de qualquer função assíncrona é impl Future<Output = T> , onde T é o tipo de retorno interno. No entanto, esse futuro também captura as vidas úteis de quaisquer argumentos de entrada em si: este é o oposto do padrão para impl Trait, que não é assumido para capturar nenhuma vida de entrada, a menos que você as especifique. Em outras palavras, usar o tipo de retorno externo significaria que as funções assíncronas nunca se beneficiaram da elisão vitalícia (a menos que fizéssemos algo ainda mais incomum, como fazer com que as regras de eliminação vitalícia funcionassem de maneira diferente para funções assíncronas e outras funções).

Decidimos que, dado o quão prolixo e francamente confuso seria escrever o tipo de retorno externo, não valia a pena dar sinais extras de que isso retorna um futuro para exigir que os usuários o escrevam.

Pedido de destruidor

A ordem dos destruidores em contextos assíncronos é a mesma que em contextos não assíncronos. As regras exatas são um pouco complicadas e fora do escopo aqui, mas, em geral, os valores são destruídos quando saem do escopo. Isso significa, porém, que eles continuam a existir por algum tempo depois de serem usados, até que sejam limpos. Se esse tempo incluir instruções de espera, esses itens precisam ser preservados no estado do futuro para que seus destruidores possam ser executados no momento apropriado.

Poderíamos, como uma otimização para o tamanho dos estados futuros, em vez disso reordenar os destruidores para serem anteriores em alguns ou todos os contextos (por exemplo, argumentos de função não utilizados podem ser descartados imediatamente, em vez de serem armazenados no estado do futuro). No entanto, decidimos não fazer isso. A ordem dos destruidores pode ser uma questão espinhosa e confusa para os usuários e, às vezes, muito significativa para a semântica do programa. Decidimos renunciar a essa otimização em favor de garantir uma ordem do destruidor que seja a mais direta possível - a mesma ordem do destruidor se todas as palavras-chave async e await fossem removidas.

(Algum dia, podemos estar interessados ​​em buscar maneiras de marcar destruidores como puros e reordenáveis. Isso é trabalho de design futuro que tem implicações não relacionadas a async / await também.)

Referência:

Aguarde a sintaxe do operador

Um grande desvio dos recursos async / await de outras linguagens é a sintaxe de nosso operador await. Este tem sido o assunto de uma enorme discussão, mais do que qualquer outra decisão que tomamos no design do Rust.

Desde 2015, Rust tem um operador postfix ? para tratamento ergonômico de erros. Desde muito antes de 1.0, Rust também tinha um operador postfix . para acesso a campos e chamadas de método. Como o principal caso de uso para futuros é realizar algum tipo de IO, a grande maioria dos futuros avalia Result com alguns
tipo de erro. Isso significa que, na prática, quase todas as operações de espera são sequenciadas com ? ou uma chamada de método após ela. Dada a precedência padrão para operadores de prefixo e pós-fixação, isso teria feito com que quase todos os operadores de espera fossem escritos (await future)? , o que consideramos como altamente antiergonômico.

Decidimos então usar uma sintaxe pós-fixada, que combina muito bem com os operadores ? e . . Depois de considerar muitas opções sintáticas diferentes, escolhemos usar o operador . seguido pela palavra-chave await.

Referência:

Suportando executores single e multithreaded

O Rust foi projetado para tornar a escrita de programas simultâneos e paralelos mais fácil, sem impor custos às pessoas que escrevem programas que rodam em um único thread. É importante poder executar funções assíncronas tanto em executores single-threaded quanto em executores multithread. A principal diferença entre esses dois casos de uso é que os executores multiencadeados limitarão os futuros que podem gerar por Send , e os executores singlethreads não.

Semelhante ao comportamento existente da sintaxe impl Trait , as funções assíncronas "vazam" as características automáticas do futuro que retornam. Ou seja, além de observar que o tipo de retorno externo é um futuro, o chamador também pode observar se aquele tipo é Send ou Sync, a partir de um exame de seu corpo. Isso significa que quando o tipo de retorno de um fn assíncrono é agendado para um executor multithread, ele pode verificar se isso é seguro ou não. No entanto, o tipo não precisa ser Send e, portanto, os usuários em executores de thread único podem aproveitar as vantagens de primitivos de thread único de maior desempenho.

Havia alguma preocupação de que isso não funcionasse bem quando as funções assíncronas fossem expandidas em métodos, mas após alguma discussão, foi determinado que a situação não seria significativamente diferente.

Referência:

Bloqueadores de estabilização conhecidos

Tamanho do estado

Problema: # 52924

A maneira como a transformação assíncrona em uma máquina de estado é implementada atualmente não é ideal, fazendo com que o estado se torne muito maior do que o necessário. É possível, porque o tamanho do estado na verdade cresce superlinearmente, acionar estouros de pilha na pilha real à medida que o tamanho do estado aumenta do que o tamanho de um encadeamento normal do sistema. Melhorar este codegen para que o tamanho seja mais razoável, pelo menos não ruim o suficiente para causar estouro de pilha em uso normal, é uma correção de bug de bloqueio.

Múltiplas vidas em funções assíncronas

Problema: # 56238

As funções assíncronas devem ser capazes de ter vários tempos de vida em sua assinatura, todos os quais são "capturados" no futuro para os quais a função será avaliada quando for chamada. No entanto, a redução atual para impl Future dentro do compilador não oferece suporte a vários tempos de vida de entrada; uma refatoração mais profunda é necessária para fazer
Este trabalho. Como os usuários são muito propensos a escrever funções com vários (provavelmente todos elididos) tempos de vida de entrada, esta é uma correção de bug de bloqueio.

Outros problemas de bloqueio:

Rótulo

Trabalho futuro

Todos esses são extensões conhecidas e de alta prioridade para o MVP que pretendemos começar a trabalhar assim que enviarmos a versão inicial do async / await.

Fechamentos assíncronos

No RFC inicial, também oferecemos suporte ao modificador assíncrono como um modificador em literais de fechamento, criando funções assíncronas anônimas. No entanto, a experiência com esse recurso mostrou que ainda há uma série de questões de design a serem resolvidas antes que nos sintamos confortáveis ​​para estabilizar este caso de uso:

  1. A natureza da captura de variáveis ​​torna-se mais complicada em fechamentos assíncronos e exige algum suporte sintático.
  2. A abstração de funções assíncronas com tempos de vida de entrada atualmente não é possível e pode exigir algum idioma adicional ou suporte de biblioteca.

Suporte sem STD

A implementação atual do operador de espera requer que o TLS passe o waker para baixo enquanto pesquisa o futuro interno. Este é essencialmente um "hack" para fazer a sintaxe funcionar em sistemas com TLS o mais rápido possível. A longo prazo, não temos intenção de nos comprometer com esse uso de TLS e preferiríamos passar o waker como um argumento de função normal. No entanto, isso requer mudanças mais profundas no código de geração da máquina de estado para que ele possa lidar com a aceitação de argumentos.

Embora não estejamos bloqueando a implementação dessa mudança, consideramos isso de alta prioridade, pois evita o uso de assíncrono / espera em sistemas sem suporte TLS. Este é um problema de implementação puro: nada no design do sistema requer o uso de TLS.

Métodos de característica assíncronos

No momento, não permitimos funções ou métodos associados assíncronos em características; este é o único lugar onde você pode escrever fn mas não async fn . Métodos assíncronos seriam claramente uma abstração poderosa e queremos apoiá-los.

Um método assíncrono seria funcionalmente tratado como um método que retorna um tipo associado que implementaria o futuro; cada método assíncrono geraria um tipo futuro exclusivo para a máquina de estado em que esse método se traduz.

No entanto, como esse futuro capturaria todas as entradas, qualquer tempo de vida de entrada ou parâmetros de tipo também precisariam ser capturados nesse estado. Isso é equivalente a um conceito denominado tipos associados genéricos , um recurso que há muito desejávamos, mas ainda não implementamos adequadamente. Assim, a resolução de métodos assíncronos está ligada à resolução de tipos genéricos associados.

Existem também questões pendentes de design. Por exemplo, os métodos assíncronos são intercambiáveis ​​com métodos que retornam tipos futuros que teriam a mesma assinatura? Além disso, os métodos assíncronos apresentam problemas adicionais em relação às características automáticas, visto que pode ser necessário exigir que o futuro retornado por algum método assíncrono implemente uma característica automática quando você abstrair sobre uma característica com um método assíncrono.

Assim que tivermos esse suporte mínimo, há outras considerações de design para extensões futuras, como a possibilidade de tornar os métodos assíncronos "seguros para o objeto".

Geradores e geradores assíncronos

Temos um recurso gerador instável que usa a mesma transformação de máquina de estado de co-rotina para pegar funções que geram vários valores e transformá-los em máquinas de estado. O caso de uso mais óbvio para esse recurso é criar funções que compilam para "iteradores", assim como as funções assíncronas compilam para
futuros. Da mesma forma, poderíamos compor esses dois recursos para criar geradores assíncronos - funções que compilam para "fluxos", o equivalente assíncrono de iteradores. Existem casos de uso realmente claros para isso na programação de rede, que geralmente envolve fluxos de mensagens enviadas entre sistemas.

Os geradores têm muitas questões de design em aberto porque são um recurso muito flexível com muitas opções possíveis. O projeto final para geradores em Rust em termos de sintaxe e APIs de biblioteca ainda está muito incerto.

A-async-await AsyncAwait-Focus F-async_await I-nominated T-lang disposition-merge finished-final-comment-period

Comentários muito úteis

O período de comentários final, com uma disposição para mesclar , conforme a revisão acima , agora está completo .

Como representante automatizado do processo de governança, gostaria de agradecer ao autor por seu trabalho e a todos que contribuíram.

O RFC será mesclado em breve.

Todos 58 comentários

@rfcbot fcp merge

O membro da equipe @withoutboats propôs fundir isso. A próxima etapa é revisada pelo restante dos membros da equipe marcados:

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [] @pnkfelix
  • [x] @scottmcm
  • [x] @withoutboats

Preocupações:

Assim que a maioria dos revisores aprovar (e no máximo 2 aprovações estão pendentes), isso entrará em seu período final de comentários. Se você detectar uma questão importante que não foi levantada em algum momento deste processo, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me dar.

(Basta registrar os bloqueadores existentes no relatório acima para garantir que eles não escorreguem)

@rfcbot concerne a implementação-trabalho-bloqueio-estabilização

Membro da equipe ... propôs mesclar isso

Como se pode mesclar um problema do Github (não uma solicitação pull)?

@vi O bot é um pouco maluco e não verifica se é um problema ou RP :) Você pode substituir "mesclar" por "aceitar" aqui.

Uau, obrigado pelo resumo abrangente! Só tenho seguido tangencialmente, mas tenho plena certeza de que você está por dentro de tudo.

@rfcbot revisado

Seria possível adicionar explicitamente “Triage AsyncAwait-Unclear issues” aos bloqueadores de estabilização (e / ou registrar uma preocupação com isso)?

Eu tenho https://github.com/rust-lang/rust/issues/60414 que considero importante (obviamente, é o meu bug: p) e gostaria de ter pelo menos explicitamente adiado antes da estabilização :)

Gostaria apenas de expressar os agradecimentos à comunidade pelo esforço que as equipes do Rust colocaram nesse recurso! Tem havido muito design, discussão e algumas falhas na comunicação, mas pelo menos eu, e esperançosamente muitos outros, estamos confiantes de que através de tudo isso encontramos a melhor solução possível para Rust. : tada:

(Dito isso, eu gostaria de ver uma menção aos problemas com a ponte para APIs de sistema de cancelamento assíncrono e baseado em conclusão em possibilidades futuras. TL; DR eles ainda têm que passar buffers próprios. É um problema de biblioteca, mas um com menção.)

Também gostaria de ver uma menção a problemas com APIs baseadas em conclusão. (consulte este tópico interno para contexto) Considerando o IOCP e a introdução de io_uring , que pode se tornar o Caminho para IO assíncrono no Linux, acho importante ter um caminho claro para lidar com eles. Ideias hipotéticas de queda assíncrona do IIUC não podem ser implementadas com segurança, e a passagem de buffers próprios será menos conveniente e potencialmente menos eficiente (por exemplo, devido a uma localidade pior ou devido a cópias adicionais).

@newpavlov Eu implementei coisas semelhantes para o Fuchsia, e é totalmente possível fazer sem queda assíncrona. Existem algumas rotas diferentes para fazer isso, como usar o pool de recursos, em que a aquisição de um recurso tem que potencialmente esperar que algum trabalho de limpeza termine nos recursos antigos. A atual API de futuros pode e tem sido usada para resolver esses problemas de forma eficaz em sistemas de produção.

No entanto, esse problema é sobre a estabilização de async / await, que é ortogonal ao design da API de futuros, que já se estabilizou. Sinta-se à vontade para fazer mais perguntas ou abrir uma questão para discussão no contrato de recompra futuro-rs.

@Ekleog

Seria possível adicionar explicitamente “Triage AsyncAwait-Unclear issues” aos bloqueadores de estabilização (e / ou registrar uma preocupação com isso)?

Sim, isso é algo que temos feito todas as semanas. WRT esse problema específico (# 60414), acredito que é importante e adoraria vê-lo corrigido, mas ainda não fomos capazes de decidir se ele deve ou não bloquear a estabilização, especialmente porque já é observável em -> impl Trait funções.

@cramertj Obrigado! Acho que o problema do # 60414 é basicamente "o erro pode surgir muito rapidamente agora", enquanto com -> impl Trait parece que ninguém percebeu isso antes - então está tudo bem se for adiado de qualquer maneira, alguns problemas terá que :) (FWIW surgiu no código natural em uma função onde eu retorno () em um lugar e T::Assoc em outro, o que o IIRC me impediu de compilar - não verifiquei o código desde a abertura # 60414, então talvez minha lembrança esteja errada)

@Ekleog Sim, faz sentido! Eu posso definitivamente ver por que seria uma dor - criei um fluxo zulip para mergulhar mais nesse problema específico.

EDIT: deixa pra lá, eu perdi o alvo 1.38 .

@cramertj

Existem algumas rotas diferentes para fazer isso, como usar o pool de recursos, em que a aquisição de um recurso tem que potencialmente esperar que algum trabalho de limpeza termine nos recursos antigos.

Eles não são menos eficientes em comparação com a manutenção de buffers como parte do estado futuro? Minha principal preocupação é que o design atual não seja de custo zero (no sentido de que você será capaz de criar um código mais eficiente eliminando async abstração) e menos ergonômico em APIs baseadas em conclusão, e há nenhuma maneira clara de consertá-lo. Não é um show-stop de forma alguma, mas acho importante não esquecer essas deficiências no design, daí o pedido para mencioná-lo no OP.

@O duque

A equipe lang pode, é claro, julgar isso melhor do que eu, mas atrasar para 1.38 para garantir uma implementação estável pareceria muito mais sensato.

Este problema tem como alvo 1.38, consulte a primeira linha de descrição.

@huxi obrigado, eu perdi isso. Editou meu comentário.

@newpavlov

Eles não são menos eficientes em comparação com a manutenção de buffers como parte do estado futuro? Minha principal preocupação é que o design atual não seja um custo zero (no sentido de que você será capaz de criar um código mais eficiente eliminando a abstração assíncrona) e menos ergonômico em APIs baseadas em preenchimento, e não há uma maneira clara de consertar isto. Não é um show-stop de forma alguma, mas acho importante não esquecer essas deficiências no design, daí o pedido para mencioná-lo no OP.

Não, não necessariamente, mas vamos mover esta discussão para um problema em um thread separado, uma vez que não está relacionado à estabilização de async / await.

(Dito isso, gostaria de ver uma menção aos problemas com a ponte para APIs de sistema de cancelamento assíncrono e baseado em conclusão em possibilidades futuras. TL; DR eles ainda têm que passar buffers próprios. É um problema de biblioteca, mas um com menção.)

Também gostaria de ver uma menção a problemas com APIs baseadas em conclusão. (consulte este tópico interno para contexto) Considerando o IOCP e a introdução de io_uring, que pode se tornar o caminho para IO assíncrono no Linux, acho importante ter um caminho claro para lidar com eles.

Eu concordo com Taylor que discutir projetos de API neste espaço de problema estaria fora do tópico, mas eu quero abordar um aspecto específico desses comentários (e esta discussão sobre io_uring em geral) que é relevante para a estabilização assíncrona / espera: o problema de cronometragem.

io_uring é uma interface que está chegando ao Linux neste ano de 2019. O projeto Rust está trabalhando na abstração de futuros desde 2015, quatro anos atrás. A escolha fundamental para favorecer uma pesquisa baseada em uma API baseada em conclusão ocorreu durante 2015 e 2016. No RustCamp em 2015, Carl Lerche falou sobre por que ele fez essa escolha em mio, a abstração IO subjacente. Nesta postagem do blog em 2016, Aaron Turon falou sobre os benefícios da criação de abstrações de nível superior. Essas decisões foram tomadas há muito tempo e não poderíamos ter chegado ao ponto que estamos agora sem elas.

As sugestões de que devemos revisitar nosso modelo de futuros subjacentes são sugestões de que devemos voltar ao estado em que estávamos há 3 ou 4 anos e começar a partir desse ponto. Que tipo de abstração poderia cobrir um modelo de IO baseado em conclusão sem introduzir sobrecarga para primitivas de nível superior, como Aaron descreveu? Como mapearemos esse modelo para uma sintaxe que permita aos usuários escrever "Rust + anotações secundárias normais" da mesma forma que async / await faz? Como seremos capazes de lidar com a integração disso em nosso modelo de memória, como fizemos para essas máquinas de estado com pino? Tentar fornecer respostas a essas perguntas seria fora do tópico deste tópico; o ponto é que respondê-los e provar as respostas corretas é trabalho. O que equivale a uma sólida década de anos de trabalho entre os diferentes contribuintes, até agora, teria que ser refeito novamente.

O objetivo do Rust é enviar um produto que as pessoas possam usar, e isso significa que temos que enviar . Nem sempre podemos parar para olhar para o futuro, o que pode se tornar um grande negócio no próximo ano, e reiniciar nosso processo de design para incorporar isso. Fazemos o melhor que podemos com base na situação em que nos encontramos. Obviamente, pode ser frustrante sentir que quase não perdemos algo importante, mas do jeito que está, também não temos uma visão completa a) de qual será o melhor resultado para lidar com io_uring será, b) quão importante será o io_uring no ecossistema como um todo. Não podemos reverter 4 anos de trabalho com base nisso.

Já existem limitações semelhantes, provavelmente ainda mais graves, do Rust em outros espaços. Quero destacar um que examinei com Nick Fitzgerald no outono passado: integração wasm GC. O plano para manipular objetos gerenciados no wasm é essencialmente segmentar o espaço de memória, de modo que existam em um espaço de endereço separado dos objetos não gerenciados (na verdade, algum dia em muitos espaços de endereço separados). O modelo de memória de Rust simplesmente não foi projetado para lidar com espaços de endereço separados e qualquer código inseguro que lide com memória heap hoje assume que há apenas 1 espaço de endereço. Embora tenhamos esboçado soluções técnicas inovadoras e tecnicamente inquebráveis, mas extremamente disruptivas, o caminho mais provável é aceitar que nossa história de GC wasm pode não ser perfeitamente ideal , porque estamos lidando com as limitações da Rust como isso existe.

Um aspecto interessante que estamos estabilizando aqui é que estamos disponibilizando estruturas autorreferenciais a partir do código seguro. O que torna isso interessante é que em um Pin<&mut SelfReferentialGenerator> , temos uma referência mutável (armazenada como um campo em Pin ) apontando para todo o estado do gerador, e temos um ponteiro dentro desse estado apontando para outro pedaço do estado. Esse ponteiro interno se aliases com a referência mutável!

A referência mutável, até onde sei, não se acostuma de fato a acessar a parte da memória para a qual o ponteiro para outro campo aponta. (Em particular, não existe um método clone ou então que leria o campo ponteiro-para usando qualquer outro ponteiro que não o autorreferencial.) Ainda assim, isso está se aproximando de ter um alias de referência mutável com algo mais do que qualquer outra coisa no ecossistema central, em particular qualquer outra coisa que vem com o próprio ferrugem. A "linha" que percorremos aqui está ficando muito tênue, e temos que ter cuidado para não perder todas essas ótimas otimizações que queremos fazer com base em referências mutáveis.

Provavelmente há pouco que possamos fazer sobre isso neste ponto, em particular porque Pin já é estável, mas acho que vale a pena apontar que isso complicará significativamente quaisquer que sejam as regras para as quais o aliasing é permitido e o que não é. Se você achou que Stacked Borrows era complicado, prepare-se para as coisas piorarem.

Cc https://github.com/rust-lang/unsafe-code-guidelines/issues/148

A referência mutável, até onde sei, não se acostuma de fato a acessar a parte da memória para a qual o ponteiro para outro campo aponta.

As pessoas têm falado sobre fazer com que todos esses tipos de corrotina implementem Debug , parece que essa conversa também deve integrar diretrizes de código inseguro para ter certeza de que é seguro depurar imprimir.

As pessoas têm falado sobre como fazer com que todos esses tipos de corrotina implementem Debug, parece que essa conversa também deve integrar diretrizes de código inseguro para ter certeza de que é seguro depurar imprimir.

De fato. Tal implementação Debug , se imprimir os campos auto-referenciados, provavelmente proibiria otimizações baseadas em referência de nível MIR dentro de geradores.

Atualização sobre bloqueadores:

Os dois bloqueadores de alto nível fizeram um grande progresso e podem estar concluídos (?). Mais informações de @cramertj @tmandry e @nikomatsakis sobre isso seria ótimo:

  • O problema de várias vidas úteis deve ter sido corrigido até # 61775
  • A questão do tamanho é mais ambígua; sempre haverá mais otimizações a fazer, mas acho que o fruto mais fácil de evitar armas de aumento exponenciais óbvias já foi resolvido.

Isso deixa a documentação e os testes como os principais bloqueadores na estabilização desse recurso. @Centril expressou consistentemente preocupações de que o recurso não foi bem testado ou polido o suficiente; @Centril existe algum lugar em que você enumerou preocupações específicas que podem ser verificadas para conduzir esse recurso à estabilização?

Não tenho certeza se alguém está dirigindo a documentação. Qualquer pessoa que queira se concentrar em melhorar a documentação da árvore no livro, referência, etc., estaria prestando um grande serviço! A documentação fora da árvore, como no repo futuro ou no areweasyncyet, tem um pouco de tempo extra.

A partir de hoje, temos 6 semanas até que a versão beta seja cortada, então digamos que temos 4 semanas (até 1º de agosto) para fazer essas coisas e ter certeza de que não perderemos 1,38.

A questão do tamanho é mais ambígua; sempre haverá mais otimizações a fazer, mas acho que o fruto mais fácil de evitar armas de aumento exponenciais óbvias já foi resolvido.

Eu acredito que sim, e alguns outros também foram fechados recentemente; mas existem outros problemas de bloqueio .

@Centril existe algum lugar em que você enumerou preocupações específicas que podem ser verificadas para conduzir esse recurso à estabilização?

Há um papel da caixa de depósito com uma lista de coisas que gostaríamos de testar e https://github.com/rust-lang/rust/issues/62121. Fora isso, tentarei reavaliar as áreas que acho que não foram testadas o mais rápido possível. Dito isso, algumas áreas agora estão muito bem testadas.

Qualquer pessoa que queira se concentrar em melhorar a documentação da árvore no livro, referência, etc., estaria prestando um grande serviço!

De fato; Eu ficaria feliz em revisar os PRs para a referência. Também cc @ehuss.


Eu também gostaria de mover async unsafe fn do MVP para seu próprio portão de recursos, porque acho que a) ele viu pouco uso, b) não foi particularmente bem testado, c) ele se comporta de maneira estranha porque o .await point não é onde você escreve unsafe { ... } e isso é compreensível a partir de um "ponto de vista de implementação com vazamento", mas não tanto de um ponto de vista de efeitos, d) teve pouca discussão e não foi incluído no RFC nem este relatório e e) fizemos isso com const fn e funcionou bem. (Posso escrever o recurso de bloqueio de relações públicas)

Eu estou bem em desestabilizar async unsafe fn , embora eu seja cético quanto a nós acabar com um design diferente do que o atual. Mas parece sensato nos dar tempo para descobrir isso!

Eu criei https://github.com/rust-lang/rust/issues/62500 para mover async unsafe fn para um portão de recurso distinto e o listei como um bloqueador. Provavelmente deveríamos criar um problema de rastreamento adequado também, eu acho.

Estou muito cético de que chegaremos a um design diferente para async unsafe fn e estou surpreso com a decisão de não incluí-lo na rodada inicial de estabilização. Eu escrevi uma série de async fn s que não são seguros e farão com que sejam async fn really_this_function_is_unsafe() ou algo assim, suponho. Isso parece uma regressão em uma expectativa básica que os usuários do Rust têm em termos de serem capazes de definir funções que requerem unsafe { ... } para serem chamadas. Ainda outro portal de recursos contribuirá para a impressão de que async / await está inacabado.

@cramertj parece que devemos discutir! Criei um tópico Zulip para ele , para tentar evitar que esse problema de rastreamento fique muito sobrecarregado.

Com relação aos tamanhos futuros, os casos que afetam cada await ponto são otimizados. O último problema que eu conheço é o # 59087, onde qualquer empréstimo de um futuro antes de esperar pode dobrar o tamanho alocado para esse futuro. Isso é muito lamentável, mas ainda um pouco melhor do que onde estávamos antes.

Tenho uma ideia de como consertar esse problema, mas a menos que seja mais comum do que imagino, provavelmente não deve ser um bloqueador para um MVP estável.

Dito isso, ainda preciso examinar o impacto dessas otimizações no Fuchsia (que foi bloqueado por um tempo, mas deve ser resolvido hoje ou amanhã). É bem possível que descubramos mais casos e precisamos decidir se algum deles deve ser bloqueado.

@cramertj (Lembrete: eu uso async / await e quero estabilizar o mais rápido possível) Seu argumento soa como um argumento para atrasar a estabilização de async / await, não para estabilizar async unsafe agora, sem experimentação e reflexão adequadas.

Especialmente porque não foi incluído no RFC, e potencialmente irá desencadear outra tempestade de merda “impl trait na posição de argumento” se for forçado a sair desta forma.

[Nota lateral que realmente não merece discussão aqui: para “Mais um portão de recurso contribuirá para a impressão de que async / await está inacabado”, eu encontrei um bug a cada poucas horas de uso de async / await, espalhado por poucos meses legitimamente necessários para a equipe rustc consertá-los, e é o que me faz dizer que está inacabado. O último foi corrigido há alguns dias, e eu realmente espero não descobrir outro quando tentar novamente compilar meu código com um rustc mais recente, mas ...]

Seu argumento soa como um argumento para atrasar a estabilização de async / await, não para estabilizar async inseguro agora, sem experimentação e reflexão adequadas.

Não, não é um argumento para isso. Eu acredito que async unsafe está pronto, e não consigo imaginar nenhum outro design para ele. Acredito que haja apenas consequências negativas em não incluí-lo neste lançamento inicial. Não acredito que atrasar async / await como um todo, nem async unsafe especificamente, produzirá um resultado melhor.

não consigo imaginar qualquer outro design para ele

Um design alternativo, embora um que definitivamente exija extensões complicadas: async unsafe fn é unsafe para .await , não para call() . O raciocínio por trás disso é que _nada inseguro pode ser feito_ no ponto onde o async fn é chamado e cria o impl Future . Tudo que essa etapa faz é colocar os dados em uma estrutura (na verdade, todos os async fn são const para chamar). O verdadeiro ponto de insegurança é avançar o futuro com poll .

(imho, se unsafe for imediato, unsafe async fn faz mais sentido, e se unsafe atrasar, async unsafe fn faz mais sentido.)

Claro, se nunca tivermos uma maneira de dizer, por exemplo, unsafe Future onde todos os métodos de Future não são seguros para chamar, então "içar" o unsafe para a criação do impl Future , e o contrato desse unsafe é usar o futuro resultante de forma segura. Mas isso também pode ser feito quase que trivialmente sem unsafe async fn apenas "desugaring" manualmente em um bloco async : unsafe fn os_stuff() -> impl Future { async { .. } } .

Além disso, porém, há uma questão de se realmente existe uma maneira de ter invariantes que precisam ser mantidos uma vez que poll ing inicie e que não precisem ser mantidos na criação. É um padrão comum em Rust que você use um construtor unsafe para um tipo seguro (por exemplo, Vec::from_raw_parts ). Mas a chave é que, após a construção, o tipo _não_ pode ser mal utilizado; o escopo unsafe acabou. Esse escopo de insegurança é a chave para as garantias de Rust. Se você introduzir um unsafe async fn que embala um seguro impl Future com requisitos de como / quando ele é pesquisado e, em seguida, passá-lo para o código seguro, esse código seguro está repentinamente dentro de sua barreira de insegurança. E isso é _muito_ provável de acontecer assim que você usar este futuro de qualquer maneira que não seja imediatamente aguardá-lo, já que provavelmente passará por _algumas_ combinações externas.

Eu acho que o TL; DR disso é que definitivamente há cantos de async unsafe fn que devem ser discutidos adequadamente antes de estabilizá-los, especialmente com a direção de const Trait potencialmente sendo introduzidos (eu tenho um blog de rascunho postagem sobre generalizar isso para um "sistema de 'efeitos' fraco" com qualquer palavra-chave de modificação fn ). No entanto, unsafe async fn pode realmente ser claro o suficiente sobre a "ordem" / "posicionamento" de unsafe para estabilizar.

Eu acredito que um traço unsafe Future baseado em efeitos não está apenas fora do alcance de qualquer coisa que sabemos expressar na linguagem ou no compilador hoje, mas que no final das contas seria um design pior devido ao efeito adicional polimorfismo que exigiria que os combinadores tivessem.

nada que não seja seguro pode ser feito no ponto em que o fn assíncrono é chamado e cria o futuro impl. Tudo o que essa etapa faz é colocar os dados em uma estrutura (na verdade, todos os fn assíncronos são constantes para chamar). O verdadeiro ponto de insegurança é avançar o futuro com pesquisas.

É verdade que, uma vez que async fn não pode executar nenhum código de usuário antes de ser .await ed, qualquer comportamento indefinido provavelmente será atrasado até que .await seja chamado. Eu acho, entretanto, que há uma distinção importante entre o ponto de UB e o ponto de unsafe ty. O ponto real de unsafe ty é onde um autor de API decide que um usuário precisa prometer que um conjunto de invariantes não estaticamente verificáveis ​​será atendido, mesmo se o resultado dessas invariantes sendo violadas não causaria UB até mais tarde em algum outro código seguro. Um exemplo comum disso é uma função unsafe para criar um valor que implementa uma característica com métodos seguros (exatamente o que é). Eu já vi isso ser usado para garantir que, por exemplo, Visitor -tipos de implementação de estreitos cujas implementações dependem de unsafe invariantes podem ser usados ​​corretamente, exigindo unsafe para construir o tipo. Outros exemplos incluem slice::from_raw_parts , que por si só não causará UB (invariantes de validade de tipo à parte), mas acessos ao slice resultante sim.

Não acredito que async unsafe fn represente um caso único ou interessante aqui - segue um padrão bem estabelecido para realizar unsafe comportamentos por trás de uma interface segura, exigindo um unsafe construtor.

@cramertj O fato de você ter que argumentar a favor disso (e não estou sugerindo que a solução atual seja ruim, ou que eu tenha uma ideia melhor) significa, para mim, que esse debate deveria ser em um coloque as pessoas que se preocupam com a ferrugem devem seguir: o repositório RFC.

Como um lembrete, uma citação de seu leia-me:

Você precisa seguir este processo se [...]:

  • Qualquer alteração semântica ou sintática no idioma que não seja uma correção de bug.
  • [... e também coisas não citadas]

Não estou dizendo que qualquer mudança no design atual irá acontecer. Na verdade, pensar nisso por alguns minutos me faz pensar que é provavelmente o melhor design que eu poderia pensar. Mas o processo é o que nos permite evitar que nossas crenças se tornem um perigo para o Rust, e estamos perdendo a sabedoria de muitas pessoas que seguem o repositório RFC, mas não leem todos os problemas por não seguir o processo aqui.

Às vezes, pode fazer sentido não seguir o processo. Aqui não vejo urgência que justifique ignorar o processo apenas para evitar cerca de 2 semanas de atraso do FCP.

Então, por favor, deixe a ferrugem ser honesta com sua comunidade sobre as promessas que faz em seu próprio leia-me, e apenas mantenha esse recurso abaixo de um portão de recurso até que haja pelo menos um RFC aceito e, com sorte, mais algum uso dele à solta. Não importa se é o portão de recurso assíncrono / espera inteiro ou apenas um portão de recurso inseguro-async, mas apenas não estabilize algo que (AFAIK) viu pouco uso além do async-wg e mal é conhecido no comunidade geral.

Estou escrevendo uma primeira passagem no material de referência para o livro. Ao longo do caminho, percebi que o RFC assíncrono espera diz que o comportamento do operador ? ainda não foi determinado. E ainda parece funcionar bem em um bloco assíncrono ( playground ). Devemos mover isso para um portão de recursos separado? Ou isso foi resolvido em algum momento? Não vi no relatório de estabilização, mas talvez não tenha percebido.

(Eu também fiz essa pergunta no Zulip e preferiria respostas lá, pois é mais fácil de gerenciar para mim.)

Sim, foi discutido e resolvido junto com o comportamento de return , break , continue et. al. que todos fazem "a única coisa possível" e se comportam como fariam dentro de uma tampa.

let f = unsafe { || {...} }; também é seguro para chamar e IIRC é equivalente a mover unsafe para dentro do fechamento.
Mesma coisa para unsafe fn foo() -> impl Fn() { || {...} } .

Isso, para mim, é precedente o suficiente para "a coisa insegura acontece depois de sair do escopo unsafe ".

O mesmo vale para outros lugares. Como apontado anteriormente, unsafe nem sempre está onde o UB potencial estaria. Exemplo:

    let mut vec: Vec<u32> = Vec::new();

    unsafe { vec.set_len(100); }      // <- unsafe

    let val = vec.get(5).unwrap();     // <- UB
    println!("{}", val);

Só me parece um mal-entendido de inseguro - inseguro não marca que "uma operação insegura ocorre aqui dentro" - marca "Estou garantindo que mantenho as invariáveis ​​necessárias aqui." Embora você possa manter as invariantes no ponto de espera, porque não envolve parâmetros variáveis, não é um site muito óbvio para verificar se você mantém as invariantes. Faz muito mais sentido e é muito mais consistente com a forma como todas as nossas abstrações inseguras funcionam, para garantir que você mantenha as invariáveis ​​no local da chamada.

Isso está relacionado ao motivo pelo qual pensar em inseguro como um efeito leva a intuições imprecisas (como Ralf argumentou quando essa ideia foi levantada pela primeira vez no ano passado). A insegurança é especificamente, intencionalmente, não infecciosa. Embora você possa escrever funções inseguras que chamam outras funções inseguras e apenas encaminham suas invariantes para cima na pilha de chamadas, esta não é a maneira normal de usar inseguro e, na verdade, é um marcador sintático usado para definir contratos em valores e verificar manualmente se você os sustenta.

Portanto, não é o caso de que toda decisão de design precise de um RFC completo, mas estamos trabalhando para tentar fornecer mais clareza e estrutura sobre como as decisões são tomadas. A lista dos principais pontos de decisão na abertura desta edição é um exemplo disso. Usando as ferramentas disponíveis para nós, gostaria de fazer uma tentativa em um ponto de consenso estruturado em torno desta questão de fns assíncronos inseguros, portanto, esta é uma postagem de resumo com uma enquete.

async unsafe fn

fns não seguros assíncronos são funções assíncronas que só podem ser chamadas dentro de um bloco não seguro. Dentro de seu corpo é tratado como um escopo inseguro. O design alternativo primário seria tornar o fns inseguro assíncrono inseguro para aguardar , em vez de chamar. Há uma série de razões sólidas para preferir o design em que não é seguro chamar:

  1. É sintaticamente consistente com o comportamento de fns inseguros não assíncronos, que também não são seguros para chamadas.
  2. É mais consistente com o modo como funciona inseguro em geral. Uma função insegura é uma abstração que depende de alguns invariantes sendo mantidos por seu chamador. Ou seja, não é o caso de marcar "onde a operação insegura acontece", mas "onde o invariante é garantido para ser mantido." É muito mais sensato verificar se os invariantes são mantidos no local da chamada, onde os argumentos são realmente especificados, do que no local de espera, separado de quando os argumentos foram selecionados e verificados. Isso é muito normal para funções não seguras em geral, que muitas vezes determinam algum estado que outras funções seguras esperam estar corretas
  3. É mais consistente com a noção de desugar de assinaturas fn assíncronas, onde você pode modelar a assinatura como equivalente a remover o modificador assíncrono e agrupar o tipo de retorno no futuro.
  4. A alternativa não é viável para implementar a curto ou médio prazo (ou seja, vários anos). Não há como criar um futuro inseguro de pesquisar na linguagem Rust projetada atualmente. Algum tipo de "inseguro como um efeito" seria uma mudança enorme que teria implicações de longo alcance e necessidade de lidar com a forma como ele é compatível com insegura como ela existe hoje (como, funções inseguras normais e blocos). Adicionar fns não seguros assíncronos não muda significativamente esse cenário, ao passo que fns não seguros assíncronos sob a interpretação atual de inseguro têm casos de uso práticos reais a curto e médio prazo.

@rfcbot ask lang "Aceitamos estabilizar fn assíncrono inseguro como um fn assíncrono que não é seguro chamar?"

Não tenho ideia de como fazer uma enquete com o rfcbot, mas pelo menos indiquei.

O membro da equipe @withoutboats pediu às equipes: T-lang, para um consenso sobre:

"Aceitamos estabilizar fn não seguro assíncrono como fn assíncrono que não é seguro chamar?"

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [] @joshtriplett
  • [x] @nikomatsakis
  • [] @pnkfelix
  • [] @scottmcm
  • [x] @withoutboats

@withoutboats

Eu gostaria de dar uma olhada em um ponto de consenso estruturado em torno dessa questão de fns assíncronos inseguros, então esta é uma postagem resumida com uma enquete.

Obrigado pelo artigo. A discussão me convenceu de que async unsafe fn vez que funciona todas as noites, hoje se comporta bem. (Embora alguns testes provavelmente devam ser adicionados, pois parecia esparso.) Além disso, você poderia alterar o relatório no topo com partes do seu relatório + uma descrição de como async unsafe fn se comporta?

É mais consistente com o modo como funciona inseguro em geral. Uma função insegura é uma abstração que depende de alguns invariantes sendo mantidos por seu chamador. Ou seja, não é o caso de marcar "onde a operação insegura acontece", mas "onde o invariante é garantido para ser mantido." É muito mais sensato verificar se os invariantes são mantidos no local da chamada, onde os argumentos são realmente especificados, do que no local de espera, separado de quando os argumentos foram selecionados e verificados. Isso é muito normal para funções não seguras em geral, que muitas vezes determinam algum estado que outras funções seguras esperam estar corretas

Como alguém que não está prestando muita atenção, eu concordaria e acho que a solução aqui é uma boa documentação.

Eu posso estar errado aqui, mas dado que

  • os futuros são combinatórios por natureza, é fundamental que sejam composíveis.
  • Os pontos de espera dentro de uma implementação futura geralmente são um detalhe de implementação invisível.
  • o futuro está muito distante do contexto de execução, com o usuário real talvez no meio em vez de na raiz.

parece-me que invariantes dependendo do uso / comportamento de espera específico está em algum lugar entre uma má ideia e impossível de controlar com segurança.

Se houver casos em que o valor de saída esperado é o que está envolvido na manutenção das invariáveis, presumo que o futuro poderia simplesmente ter uma saída que é um invólucro exigindo acesso inseguro, como

struct UnsafeOutput<T>(T);
impl<T> UnsafeOutput<T> {
    unsafe fn unwrap(self) -> T { self.0 }
}

Dado que unsafe ness está antes de async ness neste "início inseguro", eu ficaria muito mais feliz com o pedido do modificador sendo unsafe async fn que async unsafe fn , porque unsafe (async fn) mapeia muito mais obviamente esse comportamento do que async (unsafe fn) .

Terei prazer em aceitar qualquer um dos dois, mas sinto fortemente que a ordem de embalagem exposta aqui tem unsafe do lado de fora, e a ordem dos modificadores pode ajudar a deixar isso claro. ( unsafe é o modificador para async fn , não async o modificador para unsafe fn .)

Terei prazer em aceitar qualquer um dos dois, mas sinto fortemente que a ordem de empacotamento exposta aqui tem unsafe do lado de fora, e a ordem dos modificadores pode ajudar a deixar isso claro. ( unsafe é o modificador para async fn , não async o modificador para unsafe fn .)

Eu estava com você até o seu último ponto entre parênteses. O artigo de @withoutboats deixa bem claro para mim que, se a insegurança for tratada no site da chamada, o que você realmente tem é um unsafe fn (que por acaso é chamado em um contexto assíncrono).

Eu diria que pintamos a malha de bicicletas async unsafe fn .

Acho que async unsafe fn faz mais sentido, mas também acho que devemos aceitar gramaticalmente qualquer ordem entre assíncrono, inseguro e const. Mas async unsafe fn faz mais sentido para mim com a noção de que você remove o assíncrono e modifica o tipo de retorno para "removê-lo".

A alternativa não é viável para implementar a curto ou médio prazo (ou seja, vários anos). Não há como criar um futuro inseguro de pesquisar na linguagem Rust projetada atualmente.

FWIW, encontrei um problema semelhante que mencionei no RFC2585 quando se trata de fechamentos dentro de unsafe fn e os traços de função. Eu não esperava que unsafe async fn retornasse um Future com um método poll seguro, mas em vez disso, retornasse um UnsafeFuture com um unsafe método de votação. (*) Poderíamos então fazer .await também funcionar em UnsafeFuture s quando for usado dentro de blocos de unsafe { } , mas não de outra forma.

Essas duas características futuras seriam uma grande mudança em relação ao que temos hoje e provavelmente introduziriam muitos problemas de composição. Portanto, o navio para explorar alternativas provavelmente navegou. Particularmente porque isso seria diferente de como os traços Fn funcionam hoje (por exemplo, não temos um traço UnsafeFn ou similar, e meu problema no RFC2585 era criar um fechamento dentro de um unsafe fn retorna um encerramento que impls Fn() , ou seja, que é seguro chamar, embora esse encerramento possa chamar funções não seguras.

Criar o futuro "inseguro" ou o fechamento não é o problema, o problema é ligar para eles sem provar que fazer isso é seguro, principalmente quando seus tipos não dizem que isso deve ser feito.

(*) Podemos fornecer um implemento cobertor de UnsafeFuture para todos os Future s, e também podemos fornecer UnsafeFuture um método unsafe para "desembrulhar" a si mesmo como Future que é seguro para poll .

Aqui estão meus dois centavos:

  • A explicação de @cramertj (https://github.com/rust-lang/rust/issues/62149#issuecomment-510166207) me convence de que as funções assíncronas unsafe têm o design certo.
  • Eu prefiro muito mais uma ordem fixa das palavras-chave unsafe e async
  • Eu prefiro um pouco a ordem unsafe async fn porque a ordem parece mais lógica. Semelhante a "um carro elétrico rápido" vs "um carro elétrico rápido". Principalmente porque um async fn desugars para um fn . Portanto, faz sentido que as duas palavras-chave estejam próximas uma da outra.

Eu acho que let f = unsafe { || { ... } } deve tornar f seguro, um traço UnsafeFn nunca deve ser introduzido, e a priori .await ing e async unsafe fn devem ser seguro. Qualquer UnsafeFuture precisa de uma justificativa forte!

Tudo isso acontece porque unsafe deve ser explícito e Rust deve empurrá-lo de volta para uma terra segura. Também por este token, f 's ... _não_ deve ser um bloco inseguro, https://github.com/rust-lang/rfcs/pull/2585 deve ser adotado, e um async unsafe fn deve ter um corpo seguro.

Acho que este último ponto pode ser bastante crucial. É possível que cada async unsafe fn empregue um bloco de unsafe , mas da mesma forma a maioria se beneficiaria de alguma análise de segurança, e muitas parecem complexas o suficiente para cometer erros facilmente.

Nunca devemos ignorar o verificador de empréstimo ao capturar para fechamentos em particular.

Portanto, meu comentário aqui: https://github.com/rust-lang/rust/issues/62149#issuecomment -511116357 é uma ideia muito ruim.

Um traço UnsafeFuture exigiria que o chamador escrevesse unsafe { } para pesquisar um futuro, mas o chamador não tem ideia de quais obrigações devem ser comprovadas ali, por exemplo, se você receber um Box<dyn UnsafeFuture> é unsafe { future.poll() } seguro? Para todos os futuros? Você não pode saber. Portanto, isso seria completamente inútil, como @rpjohnst apontou em discordância para um traço UnsafeFn semelhante.

Exigir que o futuro esteja sempre seguro para a pesquisa faz sentido, e o processo de construção de um futuro que deve ser seguro para a pesquisa pode ser inseguro; Suponho que async unsafe fn seja isso. Mas, nesse caso, o fn item pode documentar o que precisa ser mantido para que o futuro retornado esteja seguro para pesquisa.

@rfcbot implementação-trabalho-bloqueio-estabilização

Pelo que sei, ainda existem 2 bloqueadores de implementação conhecidos (https://github.com/rust-lang/rust/issues/61949, https://github.com/rust-lang/rust/issues/62517) e seria ainda é bom adicionar alguns testes. Estou resolvendo minha preocupação em fazer com que o rfcbot não seja nosso bloqueador em relação ao tempo e, em seguida, bloquearemos as correções.

@rfcbot resolve implementação-trabalho-bloqueio-estabilização

: bell: Agora está entrando em seu período de comentários final , de acordo com a revisão acima . :Sino:

PR de estabilização arquivado em https://github.com/rust-lang/rust/pull/63209.

O período de comentários final, com uma disposição para mesclar , conforme a revisão acima , agora está completo .

Como representante automatizado do processo de governança, gostaria de agradecer ao autor por seu trabalho e a todos que contribuíram.

O RFC será mesclado em breve.

Um aspecto interessante que estamos estabilizando aqui é que estamos disponibilizando estruturas autorreferenciais a partir do código seguro. O que torna isso interessante é que em um Pin <& mut SelfReferentialGenerator>, temos uma referência mutável (armazenada como um campo no Pin) apontando para todo o estado do gerador, e temos um ponteiro dentro desse estado apontando para outra parte do estado . Esse ponteiro interno se aliasa com a referência mutável!

Como uma continuação disso, @comex conseguiu escrever algum código Rust assíncrono (seguro) que viola as anotações noalias do LLVM da forma como as emitimos atualmente. No entanto, parece que devido ao uso de TLS, atualmente não há erros de compilação.

Esta página foi útil?
0 / 5 - 0 avaliações