Rust: Rastreamento de problema para assíncrono / aguardar (RFC 2394)

Criado em 8 mai. 2018  ·  308Comentários  ·  Fonte: rust-lang/rust

Esse é o problema de rastreamento do RFC 2394 (rust-lang / rfcs # 2394), que adiciona sintaxe async e await ao idioma.

Estarei liderando o trabalho de implementação desta RFC, mas gostaria de receber orientação, pois tenho relativamente pouca experiência em trabalhar com rustc.

FAÇAM:

Perguntas não resolvidas:

A-async-await A-generators AsyncAwait-Triaged B-RFC-approved C-tracking-issue T-lang

Comentários muito úteis

Sobre a sintaxe: Eu realmente gostaria de ter await como uma palavra-chave simples. Por exemplo, vamos dar uma olhada em uma preocupação do blog:

Não temos certeza de qual sintaxe queremos para a palavra-chave await. Se algo é o futuro de um Resultado - como qualquer futuro de IO provavelmente será - você deseja ser capaz de aguardar e então aplicar o operador ? a ele. Mas a ordem de precedência para habilitar isso pode parecer surpreendente - await io_future? await primeiro e ? segundo, apesar de ? ser lexicamente mais limitado do que esperar.

Eu concordo aqui, mas os aparelhos são malvados. Acho que é mais fácil lembrar que ? tem precedência inferior a await e termina com ela:

let foo = await future?

É mais fácil de ler, é mais fácil de refatorar. Eu acredito que é a melhor abordagem.

let foo = await!(future)?

Permite entender melhor a ordem em que as operações são executadas, mas ao mesmo tempo é menos legível.

Eu acredito que uma vez que você obtenha que await foo? executa await primeiro, você não terá problemas com isso. Provavelmente está lexicamente mais amarrado, mas await está no lado esquerdo e ? está no direito. Portanto, ainda é lógico o suficiente para await primeiro e manipular Result depois disso.


Se houver desacordo, por favor, expresse-o para que possamos discutir. Eu não entendo o que significa downvote silencioso. Todos nós desejamos o bem à Ferrugem.

Todos 308 comentários

A discussão aqui parece ter morrido, portanto, vinculá-la aqui como parte da questão de sintaxe await : https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

A implementação está bloqueada em # 50307.

Sobre a sintaxe: Eu realmente gostaria de ter await como uma palavra-chave simples. Por exemplo, vamos dar uma olhada em uma preocupação do blog:

Não temos certeza de qual sintaxe queremos para a palavra-chave await. Se algo é o futuro de um Resultado - como qualquer futuro de IO provavelmente será - você deseja ser capaz de aguardar e então aplicar o operador ? a ele. Mas a ordem de precedência para habilitar isso pode parecer surpreendente - await io_future? await primeiro e ? segundo, apesar de ? ser lexicamente mais limitado do que esperar.

Eu concordo aqui, mas os aparelhos são malvados. Acho que é mais fácil lembrar que ? tem precedência inferior a await e termina com ela:

let foo = await future?

É mais fácil de ler, é mais fácil de refatorar. Eu acredito que é a melhor abordagem.

let foo = await!(future)?

Permite entender melhor a ordem em que as operações são executadas, mas ao mesmo tempo é menos legível.

Eu acredito que uma vez que você obtenha que await foo? executa await primeiro, você não terá problemas com isso. Provavelmente está lexicamente mais amarrado, mas await está no lado esquerdo e ? está no direito. Portanto, ainda é lógico o suficiente para await primeiro e manipular Result depois disso.


Se houver desacordo, por favor, expresse-o para que possamos discutir. Eu não entendo o que significa downvote silencioso. Todos nós desejamos o bem à Ferrugem.

Tenho opiniões mistas sobre await ser uma palavra-chave, @Pzixel. Embora certamente tenha um apelo estético e talvez seja mais consistente, dado que async é uma palavra-chave, "aumento de palavras-chave" em qualquer idioma é uma preocupação real. Dito isso, ter async sem await faz algum sentido, em termos de recursos? Se isso acontecer, talvez possamos deixar como está. Se não, eu tenderia a fazer de await uma palavra-chave.

Acho que é mais fácil lembrar que ? tem precedência inferior a await e termina com ela

Pode ser possível aprender isso e internalizá-lo, mas há uma forte intuição de que as coisas que estão se tocando são mais estreitamente ligadas do que as coisas que estão separadas por um espaço em branco, então acho que sempre seria lido errado à primeira vista na prática.

Também não ajuda em todos os casos, por exemplo, uma função que retorna Result<impl Future, _> :

let foo = await (foo()?)?;

A preocupação aqui não é simplesmente "você pode entender a precedência de um único await + ? ", mas também "como é encadear vários awaits". Portanto, mesmo que apenas escolhêssemos uma precedência, ainda teríamos o problema de await (await (await first()?).second()?).third()? .

Um resumo das opções para a sintaxe await , algumas do RFC e o resto do thread RFC:

  • Requer delimitadores de algum tipo: await { future }? ou await(future)? (isso é barulhento).
  • Simplesmente escolha uma precedência, de modo que await future? ou (await future)? faça o que é esperado (ambos parecem surpreendentes).
  • Combine os dois operadores em algo como await? future (isso é incomum).
  • Faça await postfix de alguma forma, como em future await? ou future.await? (isso não tem precedentes).
  • Use um novo sigilo como ? fez, como em future@? (isto é "ruído de linha").
  • Não use sintaxe alguma, tornando o await implícito (isso torna os pontos de suspensão mais difíceis de ver). Para que isso funcione, o ato de construir um futuro também deve ser explicitado. Este é o assunto do tópico interno que vinculei acima .

Dito isso, ter async sem await faz algum sentido, em termos de recursos?

@alexreg Sim . Kotlin funciona assim, por exemplo. Esta é a opção "espera implícita".

@rpjohnst Interessante. Bem, geralmente sou a favor de deixar async e await como recursos explícitos da linguagem, já que acho que é mais o espírito do Rust, mas não sou especialista em programação assíncrona. ..

@alexreg async / @rpjohnst classificou muito bem todas as possibilidades. Prefiro a segunda opção, concordo com outras considerações (barulhento / incomum / ...). Tenho trabalhado com código async / await nos últimos 5 anos ou algo assim, é muito importante ter essas palavras-chave de flag.

@rpjohnst

Portanto, mesmo que apenas escolhêssemos uma precedência, ainda teríamos o problema de await (await (await first ()?). Second ()?). Third () ?.

Na minha prática, você nunca escreve dois await em uma linha. Em casos muito raros, quando você precisar, simplesmente reescreva como then e não use o recurso de espera. Você pode ver que é muito mais difícil de ler do que

let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;

Então eu acho que está tudo bem se a linguagem desencoraja escrever código dessa maneira, a fim de tornar o caso primário mais simples e melhor.

hero away future await? parece interessante, embora desconhecido, mas não vejo nenhum contra-argumento lógico contra isso.

Na minha prática, você nunca escreve dois await em uma linha.

Mas isso é porque é uma má ideia, independentemente da sintaxe, ou apenas porque a sintaxe await do C # o torna feio? As pessoas fizeram argumentos semelhantes em torno de try!() (o precursor de ? ).

As versões postfix e implícita são muito menos feias:

first().await?.second().await?.third().await?
first()?.second()?.third()?

Mas isso é porque é uma má ideia, independentemente da sintaxe, ou apenas porque a sintaxe await existente do C # o torna feio?

Acho que é uma má ideia, independentemente da sintaxe, porque ter uma linha por operação async já é complexo o suficiente para entender e difícil de depurar. Tê-los acorrentados em uma única instrução parece ser ainda pior.

Por exemplo, vamos dar uma olhada no código real (eu peguei uma parte do meu projeto):

[Fact]
public async Task Should_UpdateTrackableStatus()
{
    var web3 = TestHelper.GetWeb3();
    var factory = await SeasonFactory.DeployAsync(web3);
    var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
    var request = await season.GetOrCreateRequestAsync("123");

    var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
    var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");

    await request.UpdateStatusAsync(trackableStatus);
    await request.UpdateStatusAsync(nonTrackableStatus);

    var statuses = await request.GetStatusesAsync();

    Assert.Single(statuses);
    Assert.Equal(trackableStatus, statuses.Single());
}

Mostra que na prática não vale a pena encadear await s mesmo se a sintaxe permitir, porque se tornaria completamente ilegível await apenas torna o oneliner ainda mais difícil de escrever e ler, mas eu faço acredite que não é a única razão pela qual é ruim.

As versões postfix e implícita são muito menos feias

A possibilidade de distinguir o início da tarefa e a espera da tarefa é muito importante. Por exemplo, costumo escrever código assim (novamente, um snippet do projeto):

public async Task<StatusUpdate[]> GetStatusesAsync()
{
    int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
    var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
    var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
    {
        var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
        return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
    });

    return await Task.WhenAll(tasks);
}

Aqui estamos criando N solicitações assíncronas e, em seguida, aguardando-as. Não esperamos em cada iteração de loop, mas primeiro criamos uma matriz de solicitações assíncronas e, em seguida, esperamos todas de uma vez.

Não conheço Kotlin, então talvez eles resolvam isso de alguma forma. Mas não vejo como você pode expressar isso se "correr" e "aguardar" a tarefa é a mesma coisa.


Portanto, acho que a versão implícita é impossível em linguagens muito mais implícitas como C #.
Em Rust com suas regras que nem mesmo permitem que você converta u8 implicitamente em i32 , seria muito mais confuso.

@Pzixel Sim, a segunda opção parece uma das mais preferíveis. Usei async/await em C # também, mas não muito, já que não tenho programado principalmente em C # há alguns anos. Quanto à precedência, await (future?) é mais natural para mim.

@rpjohnst Eu meio que gosto da ideia de um operador postfix, mas também estou preocupado com a legibilidade e as suposições que as pessoas farão - pode facilmente ficar confuso para um membro de um struct chamado await .

A possibilidade de distinguir o início da tarefa e a espera da tarefa é muito importante.

Pelo que vale a pena, a versão implícita faz isso. Foi discutido até a morte tanto no segmento RFC quanto no segmento interno, então não vou entrar em muitos detalhes aqui, mas a ideia básica é apenas que ele move a explicitação da tarefa aguardar para a construção da tarefa - isso não não introduz nenhuma nova implicação.

Seu exemplo seria mais ou menos assim:

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Isso é o que eu quis dizer com "para que isso funcione, o ato de construir um futuro também deve ser explicitado". É muito semelhante a trabalhar com threads em código de sincronização - chamar uma função sempre espera que ela seja concluída antes de retomar o chamador, e existem ferramentas separadas para introduzir a simultaneidade. Por exemplo, fechamentos e thread::spawn / join correspondem a blocos assíncronos e join_all / select / etc.

Para valer a pena, a versão implícita faz isso. Foi discutido até a morte tanto no segmento RFC quanto no segmento interno, então não vou entrar em muitos detalhes aqui, mas a ideia básica é apenas que ele move a explicitação da tarefa aguardar para a construção da tarefa - isso não não introduz nenhuma nova implicação.

Eu acredito que sim. Não consigo ver aqui qual fluxo estaria nesta função, onde é pontos onde a execução é interrompida até que o await seja concluído. Só vejo o bloco async que diz "olá, em algum lugar aqui há funções assíncronas, tente descobrir quais, você ficará surpreso!".

Outro ponto: a ferrugem tende a ser uma linguagem onde você pode expressar tudo, perto do bare metal e assim por diante. Gostaria de fornecer um código bastante artificial, mas acho que ilustra a ideia:

var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
   var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
   if (firstFinishedTask != b) // our task is timeouted
      throw new Exception(); // doing something
   // more logic here
}
else
{
   // more logic here
}

A ferrugem sempre tende a fornecer controle total sobre o que está acontecendo. await permitem que você especifique pontos onde o processo de continuação. Também permite que você unwrap um valor no futuro. Se você permitir a conversão implícita no lado do uso, isso terá várias implicações:

  1. Em primeiro lugar, você precisa escrever algum código sujo para apenas emular esse comportamento.
  2. Agora, RLS e IDEs devem esperar que nosso valor seja Future<T> ou o próprio T aguardado. Não é um problema com palavras-chave - ele existe, então o resultado é T , caso contrário, é Future<T>
  3. Isso torna o código mais difícil de entender. Em seu exemplo, não vejo por que ele interrompe a execução na linha get_status_updates , mas não na linha get_status_update . Eles são bastante semelhantes entre si. Portanto, ou não funciona da maneira que o código original estava ou é tão complicado que não consigo ver, mesmo quando estou bastante familiarizado com o assunto. Ambas as alternativas não tornam essa opção um favor.

Não consigo ver aqui qual fluxo estaria nesta função, onde é pontos onde a execução é interrompida até que o await seja concluído.

Sim, é isso que eu quis dizer com "isso torna os pontos de suspensão mais difíceis de ver". Se você leu o tópico interno vinculado, apresentei um argumento para explicar por que isso não é um grande problema. Você não precisa escrever nenhum código novo, apenas coloque as anotações em um lugar diferente (blocos de await async vez de await ed expressões). Os IDEs não têm problemas em dizer qual é o tipo (é sempre T para chamadas de função e Future<Output=T> para async blocos).

Também observarei que seu entendimento provavelmente está errado, independentemente da sintaxe. As funções de async do Rust b.Status != TaskStatus.RanToCompletion sempre será aprovada. Isso também foi discutido até a morte no tópico RFC, se você estiver interessado em saber por que funciona dessa maneira.

Em seu exemplo, não vejo por que ele interrompe a execução na linha get_status_updates , mas não na linha get_status_update . Eles são bastante semelhantes entre si.

Ele faz a execução de interrupção em ambos os lugares. A chave é que os blocos de async não rodam até que sejam esperados, porque isso é verdade para todos os futuros de Ferrugem, como descrevi acima. No meu exemplo, get_statuses chama (e portanto espera) get_status_updates , então no loop ele constrói (mas não espera) count futuros, então chama (e portanto espera ) join_all , momento em que esses futuros simultaneamente pagam (e, portanto, aguardam) get_status_update .

A única diferença com o seu exemplo é quando exatamente o futuro começa a correr - no seu, é durante o loop; no meu, é durante join_all . Mas esta é uma parte fundamental de como os futuros de Rust funcionam, nada a ver com a sintaxe implícita ou mesmo com async / await .

Também observarei que seu entendimento provavelmente está errado, independentemente da sintaxe. As funções assíncronas do Rust não executam nenhum código até que sejam aguardadas de alguma forma, portanto, sua verificação b.Status! = TaskStatus.RanToCompletion sempre será aprovada.

Sim, as tarefas C # são executadas de forma síncrona até o primeiro ponto de suspensão. Obrigado por apontar isso.
No entanto, isso realmente não importa porque eu ainda devo ser capaz de executar alguma tarefa em segundo plano enquanto executo o resto do método e, em seguida, verificar se a tarefa em segundo plano foi concluída. Por exemplo, pode ser

var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the same

Tive a sua ideia sobre blocos de async e, pelo que vejo, são a mesma besta, mas com mais desvantagens. Na proposta original, cada tarefa assíncrona é emparelhada com await . Com blocos de async , cada tarefa seria emparelhada com o bloco async no ponto de construção, então estamos quase na mesma situação de antes (relação 1: 1), mas um pouco pior, porque parece mais não natural e mais difícil de entender, porque o comportamento do callite torna-se dependente do contexto. Com await eu posso ver let a = foo() ou let b = await foo() e eu saberia que esta tarefa acabou de ser construída ou construída e aguardada. Se eu vir let a = foo() com blocos de async , tenho que verificar se há algum async acima, se entendi bem, porque neste caso

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Estamos esperando por todas as tarefas de uma vez enquanto aqui

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Isn't "just a construction" anymore
        task.push({
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }
    tasks 
}

Estamos executando-os um por um.

Portanto, não posso dizer qual é o comportamento exato desta parte:

let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))

Sem ter mais contexto.

E as coisas ficam mais estranhas com blocos aninhados. Sem mencionar as perguntas sobre ferramentas, etc.

o comportamento do callite torna-se dependente do contexto

Isso já é verdade com fechamentos e código de sincronização normal. Por exemplo:

// Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
    let data = do_something_synchronous();
    StatusUpdate { data }
});

vs

// Execute a block, immediately running `do_something_synchronous()`:
task.push({
    let data = do_something_synchronous();
    StatusUpdate { data }
});

Outra coisa que você deve observar em toda a proposta implícita de espera é que você não pode chamar async fn s de contextos que não sejam async . Isso significa que a sintaxe de chamada de função some_function(arg1, arg2, etc) sempre executa some_function no corpo até a conclusão antes que o chamador continue, independentemente de some_function ser async . Portanto, a entrada em um contexto async é sempre marcada explicitamente e a sintaxe de chamada de função é, na verdade, mais consistente.

Com relação à sintaxe de await: Que tal uma macro com sintaxe de método? Não consigo encontrar um RFC real para permitir isso, mas encontrei algumas discussões ( 1 , 2 ) no reddit, então a ideia não é sem precedentes. Isso permitiria que await trabalhasse na posição postfix sem torná-la uma palavra-chave / introduzindo uma nova sintaxe apenas para este recurso.

// Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()?

Existe uma biblioteca do mundo Scala que simplifica as composições de mônadas: http://monadless.io

Talvez algumas ideias sejam interessantes para Rust.

citação dos documentos:

A maioria das linguagens convencionais tem suporte para programação assíncrona usando o idioma async / await ou está implementando-o (por exemplo, F #, C # / VB, Javascript, Python, Swift). Embora útil, async / await geralmente está vinculado a uma mônada específica que representa cálculos assíncronos (Tarefa, Futuro etc.).

Esta biblioteca implementa uma solução semelhante a async / await, mas generalizada para qualquer tipo de mônada. Essa generalização é um fator importante, considerando que algumas bases de código usam outras mônadas como Task além de Future para cálculos assíncronos.

Dada uma mônada M , a generalização usa o conceito de elevar valores regulares para uma mônada ( T => M[T] ) e retirar valores de uma instância de mônada ( M[T] => T ). > Exemplo de uso:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Observe que o aumento corresponde a assíncrono e não a elevação para aguardar.

Isso já é verdade com fechamentos e código de sincronização normal. Por exemplo:

Vejo várias diferenças aqui:

  1. O contexto lambda é inevitável, mas não é para await . Com await não temos um contexto, com async temos que ter um. O primeiro vence, pois oferece os mesmos recursos, mas exige um menor conhecimento sobre o código.
  2. Lambdas tende a ser curto, várias linhas no máximo, então vemos o corpo inteiro de uma vez, e simples. async funções
  3. Lambdas raramente são aninhados (exceto para then chamadas, mas é await proposto), async blocos são aninhados freqüentemente.

Outra coisa que você deve observar na proposta de espera implícita completa é que você não pode chamar fns assíncronos de contextos não assíncronos.

Hmm, eu não percebi isso. Não parece bom, porque, na minha prática, você geralmente deseja executar de forma assíncrona a partir de um contexto não assíncrono. Em C # async é apenas uma palavra-chave que permite ao compilador reescrever o corpo da função, não afeta a interface da função de forma alguma, então async Task<Foo> e Task<Foo> são completamente intercambiáveis ​​e desacopla implementação e API.

Às vezes, você pode querer bloquear a tarefa async , por exemplo, quando quiser chamar alguma API de rede de main . Você deve bloquear (caso contrário, você retorna ao sistema operacional e o programa termina), mas você deve executar uma solicitação HTTP assíncrona. Não tenho certeza de qual solução poderia estar aqui, exceto hackear main para permitir que seja assíncrono, assim como fazemos com o tipo de retorno Result main, se você não puder chamá-lo de um main não assíncrono .

Outra consideração a favor do await atual é como ele funciona em outra linguagem popular (conforme observado por @fdietze ). Isso torna mais fácil migrar de outra linguagem, como C # / TypeScript / JS / Python e, portanto, é uma abordagem melhor em termos de atrair novas pessoas.

Eu vejo várias diferenças aqui

Você também deve perceber que a RFC principal já possui blocos de async , com a mesma semântica da versão implícita.

Não parece bom, porque, na minha prática, você geralmente deseja executar de forma assíncrona a partir de um contexto não assíncrono.

Isto não é um problema. Você ainda pode usar async blocos em não- async contextos (o que é bom, porque eles simplesmente avaliar a um F: Future como sempre), e você ainda pode desovar ou bloco no futuro usando exatamente a mesma API de antes.

Você simplesmente não pode chamar async fn s, mas em vez disso, envolva a chamada para eles em um bloco async como você faz, independentemente do contexto em que está, se quiser um F: Future fora disso.

async é apenas uma palavra-chave que permite ao compilador reescrever o corpo da função, não afeta a interface da função de forma alguma

Sim, esta é uma diferença legítima entre as propostas. Também foi coberto com o fio interno. Indiscutivelmente, ter interfaces diferentes para os dois é útil porque mostra que a versão async fn não executará nenhum código como parte da construção, enquanto a versão -> impl Future pode, por exemplo, iniciar uma solicitação antes de fornecer a você a F: Future . Também torna async fn s mais consistente com o normal fn s, em que chamar algo declarado como -> T sempre dará a você um T , independentemente de ser async .

(Você também deve observar que em Rust ainda um grande salto entre async fn e Future versão -returning, conforme descrito na RFC. O async fn versão não menciona Future em qualquer lugar em sua assinatura; e a versão manual requer impl Trait , o que acarreta alguns problemas relacionados à vida. Isso é, de fato, parte da motivação para async fn para começar.)

Torna mais fácil migrar de outra linguagem, como C # / TypeScript / JS / Python

Isso é uma vantagem apenas para a sintaxe literal await future , que é bastante problemática por si só no Rust. Qualquer outra coisa com que possamos acabar também tem uma incompatibilidade com essas linguagens, enquanto implícito await pelo menos tem a) semelhanças com Kotlin eb) semelhanças com código síncrono baseado em thread.

Sim, esta é uma diferença legítima entre as propostas. Também foi coberto com o fio interno. Indiscutivelmente, ter interfaces diferentes para os dois é útil

Eu diria que _ter interfaces diferentes para os dois tem algumas vantagens_, porque ter API dependia de detalhes de implementação não soa bem para mim. Por exemplo, você está escrevendo um contrato que é simplesmente delegar uma chamada para um futuro interno

fn foo(&self) -> Future<T> {
   self.myService.foo()
}

E então você só quer adicionar algum registro

async fn foo(&self) -> T {
   let result = await self.myService.foo();
   self.logger.log("foo executed with result {}.", result);
   result
}

E se torna uma mudança significativa. Uau?

Isso é uma vantagem apenas para a sintaxe literal de espera futura, que é bastante problemática por si só no Rust. Qualquer outra coisa com que possamos acabar também tem uma incompatibilidade com essas linguagens, enquanto implícito await pelo menos tem a) semelhanças com Kotlin eb) semelhanças com código síncrono baseado em thread.

É uma vantagem para qualquer sintaxe await , await foo / foo await / foo@ / foo.await / ... assim que você perceber que é o mesma coisa, a única diferença é que você coloca antes / depois ou tem um sigilo em vez de uma palavra-chave.

Você também deve observar que no Rust ainda há um grande salto entre fn assíncrono e a versão com retorno futuro, conforme descrito no RFC

Eu sei disso e isso me inquieta muito.

E se torna uma mudança significativa.

Você pode contornar isso retornando um bloco async . Sob a proposta de espera implícita, seu exemplo se parece com este:

fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
    async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}

E com registro:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        result
    }
}

O maior problema de ter essa distinção surge durante a transição do ecossistema de futuras implementações manuais e combinadores (a única maneira hoje) para assíncrono / aguardar. Mas, mesmo assim, a proposta permite que você mantenha a interface antiga e forneça uma nova interface assíncrona ao lado dela. C # está repleto desse padrão, por exemplo.

Bem, isso parece razoável.

No entanto, acredito que tal implicitude (não vemos se foo() aqui é uma função assíncrona ou de sincronização) leva aos mesmos problemas que surgiram em protocolos como COM + e foi uma razão para o WCF ser implementado como era . As pessoas tinham problemas quando as solicitações remotas assíncronas pareciam chamadas de métodos simples.

Este código parece perfeitamente bem, exceto que não consigo ver se alguma solicitação é assíncrona ou sincronizada. Acredito que seja uma informação importante. Por exemplo:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        let bars: Vec<Bar> = Vec::new();
        for i in 0..100 {
           bars.push(self.my_other_service.bar(i, result));
        }
        result
    }
}

É crucial saber se bar é uma função de sincronização ou assíncrona. Costumo ver await no loop como um marcador de que esse código deve ser alterado para obter um melhor desempenho durante a carga e o desempenho. Este é um código que analisei ontem (o código não é ideal, mas é uma das iterações de revisão):

image

Como você pode ver, percebi facilmente que temos um looping aguardando aqui e pedi para alterá-lo. Quando a mudança foi confirmada, obtivemos um aumento de 3x no carregamento da página. Sem await eu poderia facilmente ignorar esse mau comportamento.

Admito que não usei Kotlin, mas da última vez que olhei para essa linguagem, parecia ser principalmente uma variante do Java com menos sintaxe, até o ponto em que era fácil traduzir mecanicamente um para o outro. Também posso imaginar por que seria apreciado no mundo do Java (que tende a ser um pouco pesado em termos de sintaxe), e estou ciente de que recentemente teve um aumento de popularidade especificamente por não ser Java (a situação do Oracle vs. Google )

No entanto, se decidirmos levar em conta a popularidade e a familiaridade, podemos dar uma olhada no que o JavaScript faz, que também é await explícito.

Dito isso, await foi introduzido nas linguagens convencionais pelo C #, que é talvez uma linguagem em que a usabilidade foi considerada de extrema importância . Em C #, as chamadas assíncronas são indicadas não apenas pela palavra-chave await , mas também pelo sufixo Async das chamadas de método. O outro recurso de linguagem que mais compartilha com await , yield return também é proeminentemente visível no código.

Por que é que? Minha opinião é que geradores e chamadas assíncronas são construções muito poderosas para deixá-los passar despercebidos no código. Existe uma hierarquia de operadores de fluxo de controle:

  • execução sequencial de instruções (implícita)
  • chamadas de função / método (bastante aparente, compare com, por exemplo, Pascal onde não há diferença no local da chamada entre uma função nula e uma variável)
  • goto (tudo bem, não é uma hierarquia estrita)
  • geradores ( yield return tende a se destacar)
  • await + Async sufixo

Observe como eles também vão de menos para mais prolixos, de acordo com sua expressividade ou poder.

É claro que outras línguas adotaram abordagens diferentes. Continuações de esquema (como em call/cc , que não é muito diferente de await ) ou macros não têm sintaxe para mostrar o que você está chamando. Para macros, Rust optou por facilitar sua visualização.

Portanto, eu diria que ter menos sintaxe não é desejável em si (existem linguagens como APL ou Perl para isso), e essa sintaxe não precisa ser apenas clichê e tem um papel importante na legibilidade.

Há também um argumento paralelo (desculpe, não consigo lembrar a fonte, mas pode ter vindo de alguém da equipe de linguagem) de que as pessoas se sentem mais confortáveis ​​com sintaxe barulhenta para novos recursos quando são novos, mas então estão bem com um menos prolixo, uma vez que acabam sendo comumente usados.


Quanto à questão de await!(foo)? vs. await foo? , estou no primeiro campo. Você pode internalizar praticamente qualquer sintaxe, no entanto, estamos acostumados a aceitar dicas de espaçamento e proximidade. Com await foo? há uma pequena chance de alguém se questionar sobre a precedência dos dois operadores, enquanto as chaves deixam claro o que está acontecendo. Salvar três personagens não vale a pena. E quanto à prática de encadear await! s, embora possa ser um idioma popular em alguns idiomas, acho que tem muitas desvantagens, como baixa legibilidade e interação com depuradores, para que valha a pena otimizar.

Salvar três personagens não vale a pena.

Em minha experiência anedótica, caracteres extras (por exemplo, nomes mais longos) não são um grande problema, mas tokens extras podem ser realmente irritantes. Em termos de analogia de CPU, um nome longo é um código linear com boa localidade - posso simplesmente digitá-lo da memória muscular - enquanto o mesmo número de caracteres quando envolve vários tokens (por exemplo, pontuação) é ramificado e cheio de erros de cache.

(Concordo plenamente que await foo? seria altamente não óbvio e devemos evitá-lo, e que ter que digitar mais tokens seria muito preferível; minha observação é apenas que nem todos os caracteres são criados iguais.)


@rpjohnst Eu acho que sua proposta alternativa poderia ter uma recepção um pouco melhor se fosse apresentada como "assíncrona explícita" em vez de "espera implícita" :-)

É crucial saber se bar é uma função de sincronização ou assíncrona.

Não tenho certeza se isso é realmente diferente de saber se alguma função é barata ou cara, ou se faz IO ou não, ou se toca algum estado global ou não. (Isso também se aplica à hierarquia de @lnicola - se as chamadas assíncronas forem concluídas exatamente como as chamadas de sincronização, elas não são realmente diferentes em termos de poder!)

Por exemplo, o fato de a chamada estar em um loop é tão, senão mais importante do que o fato de ser assíncrona. E em Rust, onde a paralelização é muito mais fácil de acertar, você poderia muito bem sugerir que loops síncronos de aparência cara sejam trocados por iteradores Rayon!

Portanto, não acho que exigir await seja realmente tão importante para obter essas otimizações. Os loops já são sempre bons lugares para procurar otimização, e async fn s já são um bom indicador de que você pode obter alguma simultaneidade IO barata. Se você perceber que está perdendo essas oportunidades, pode até escrever um lint do Clippy para "chamada assíncrona em loop" que você executa ocasionalmente. Seria ótimo ter um lint semelhante para código síncrono também!

A motivação para "assíncrono explícito" não é simplesmente "menos sintaxe", como @lnicola implica. É para tornar o comportamento da sintaxe de chamada de função mais consistente, de forma que foo() sempre execute o corpo de foo até a conclusão. De acordo com essa proposta, deixar uma anotação de fora apenas fornece código menos simultâneo, que é como praticamente todo o código já se comporta. Em "espera explícita", omitir uma anotação introduz simultaneidade acidental ou, pelo menos, intercalação acidental, o que é problemático .

Acho que sua proposta alternativa poderia ter uma recepção um pouco melhor se fosse apresentada como "assíncrona explícita" em vez de "espera implícita" :-)

O segmento é denominado "construção futura explícita, espera implícita", mas parece que o último nome permaneceu. : P

Não tenho certeza se isso é realmente diferente de saber se alguma função é barata ou cara, ou se faz IO ou não, ou se toca algum estado global ou não. (Isso também se aplica à hierarquia de @lnicola - se as chamadas assíncronas forem concluídas exatamente como as chamadas de sincronização, elas não são realmente diferentes em termos de poder!)

Acho que isso é tão importante quanto saber que a função muda algum estado, e já temos uma palavra-chave mut em ambos os lados da chamada e do chamador.

A motivação para "assíncrono explícito" não é simplesmente "menos sintaxe", como @lnicola implica. É para tornar o comportamento da sintaxe de chamada de função mais consistente, de modo que foo () sempre execute o corpo de foo até a conclusão.

Por um lado, é uma boa consideração. Por outro, você pode separar facilmente a criação futura e a execução futura. Quero dizer, se foo retorna alguma abstração que permite que você chame run e obtenha algum resultado, isso não torna foo lixo inútil que não faz nada, faz muito coisa útil: ele constrói algum objeto que você pode chamar métodos mais tarde. Isso não faz com que seja diferente. O método foo que chamamos é apenas uma caixa preta e vemos sua assinatura Future<Output=T> e ele realmente retorna um futuro. Assim, explicitamente await it quando queremos fazer isso.

O segmento é denominado "construção futura explícita, espera implícita", mas parece que o último nome permaneceu. : P

Eu pessoalmente acho que a melhor alternativa é "assíncrono explícito aguarda" :)


PS

Também fui atingido por um pensamento esta noite: você tentou se comunicar com C # LDM? Por exemplo, caras como @HaloFour , @gafter ou @CyrusNajmabadi . Pode ser realmente uma boa ideia perguntar a eles por que eles adotaram a sintaxe que adotaram. Eu proporia perguntar a caras de outras línguas também, mas eu simplesmente não os conheço :) Tenho certeza que eles tiveram vários debates sobre a sintaxe existente e eles já poderiam discuti-la muito e podem ter algumas idéias úteis.

Isso não significa que o Rust precisa ter essa sintaxe porque o C # tem, mas apenas permite tomar decisões mais ponderadas.

Eu pessoalmente acho que a melhor alternativa é "assíncrono explícito aguarda" :)

A proposta principal não é "assíncrona explícita", porém - é por isso que escolhi o nome. É "assíncrono implícito ", porque você não pode dizer à primeira vista onde a assincronia está sendo introduzida. Qualquer chamada de função não anotada pode estar construindo um futuro sem esperá-lo, mesmo que Future não apareça em nenhum lugar de sua assinatura.

Por que vale a pena, o fio internos não incluir um "assíncrona explícita aguardam explícito" alternativa, porque isso é o futuro compatível com qualquer alternativa principal. (Veja a seção final da primeira postagem.)

você tentou se comunicar com C # LDM?

O autor da RFC principal fez. O ponto principal que resultou disso, até onde me lembro, foi a decisão de não incluir Future na assinatura de async fn s. Em C #, você pode substituir Task por outros tipos para ter algum controle sobre como a função é conduzida. Mas em Rust, não temos (e não teremos) esse mecanismo - todos os futuros passarão por uma única característica, então não há necessidade de escrever essa característica todas as vezes.

Também nos comunicamos com os designers de linguagem Dart, e essa foi uma grande parte da minha motivação para escrever a proposta "assíncrona explícita". O Dart 1 teve um problema porque as funções não eram executadas na primeira espera quando chamadas (não exatamente como o Rust funciona, mas semelhante), e isso causou uma confusão tão grande que no Dart 2 eles mudaram para que as funções fossem executadas na primeira aguardar quando for chamado. O Rust não pode fazer isso por outros motivos, mas poderia executar a função inteira quando chamado, o que também evitaria essa confusão.

Também nos comunicamos com os designers de linguagem Dart, e essa foi uma grande parte da minha motivação para escrever a proposta "assíncrona explícita". O Dart 1 teve um problema porque as funções não eram executadas na primeira espera quando chamadas (não exatamente como o Rust funciona, mas semelhante), e isso causou uma confusão tão grande que no Dart 2 eles mudaram para que as funções fossem executadas na primeira aguardar quando for chamado. O Rust não pode fazer isso por outros motivos, mas poderia executar a função inteira quando chamado, o que também evitaria essa confusão.

Ótima experiência, eu não sabia disso. É bom saber que você fez um trabalho tão grande. Muito bem 👍

Também fui atingido por um pensamento esta noite: você tentou se comunicar com C # LDM? Por exemplo, caras como @HaloFour , @gafter ou @CyrusNajmabadi . Pode ser realmente uma boa ideia perguntar a eles por que eles adotaram a sintaxe que adotaram.

Tenho o prazer de fornecer qualquer informação de seu interesse. No entanto, estou apenas folheando-as. Seria possível condensar quaisquer perguntas específicas que você tenha atualmente?

Com relação à sintaxe await (isso pode ser completamente estúpido, sinta-se à vontade para gritar comigo; sou um novato em programação assíncrona e não tenho ideia do que estou falando):

Em vez de usar a palavra "esperar", não podemos introduzir um símbolo / operador, semelhante a ? . Por exemplo, poderia ser # ou @ ou qualquer outra coisa que não esteja sendo usada no momento.

Por exemplo, se fosse um operador postfix:

let stuff = func()#?;
let chain = blah1()?.blah2()#.blah3()#?;

É muito conciso e lê naturalmente da esquerda para a direita: primeiro aguarde ( # ) e, em seguida, manipule os erros ( ? ). Ele não tem o problema que a palavra-chave await do postfix tem, onde .await parece um membro de estrutura. # é claramente um operador.

Não tenho certeza se postfix é o lugar certo para estar, mas parecia assim por causa da precedência. Como prefixo:

let stuff = #func()?;

Ou até mesmo:

let stuff = func#()?; // :-D :-D

Isso já foi discutido?

(Eu percebo que isso meio que começa a se aproximar da sintaxe de "combinação aleatória de símbolos de teclado" pela qual o Perl é famoso ... :-D)

@rayvector https://github.com/rust-lang/rust/issues/50547#issuecomment -388108875, 5ª alternativa.

@CyrusNajmabadi obrigado por ter vindo. A principal questão é qual opção dentre as listadas você acha que se encaixa melhor na linguagem Rust atual, ou talvez haja alguma outra alternativa? Este tópico não é muito longo, então você pode rolar facilmente de cima para baixo rapidamente. A questão principal: o Rust deve seguir o caminho C # / TS / ... await atual ou talvez ele deva implementar o seu próprio. A sintaxe atual é algum tipo de "legado" que você gostaria de alterar de alguma forma ou se ajusta melhor ao C # e também é a melhor opção para as novas linguagens?

A principal consideração contra a sintaxe C # é a precedência do operador await foo? deve esperar primeiro e depois avaliar o operador ? , bem como a diferença que, ao contrário da execução do C #, não é executada no thread do chamador até o primeiro await , mas não começa, da mesma forma que o snippet de código atual não executa verificações de negatividade até que GetEnumerator seja chamado pela primeira vez:

IEnumerable<int> GetInts(int n)
{
   if (n < 0)
      throw new InvalidArgumentException(nameof(n));
   for (int i = 0; i <= n; i++)
      yield return i;
}

Mais detalhado em meu primeiro comentário e discussão posterior.

@Pzixel Oh, acho que perdi aquele quando estava folheando este tópico antes ...

Em qualquer caso, não vi muita discussão sobre isso, além dessa breve menção.

Existem bons argumentos a favor / contra?

@rayvector Eu argumentei um pouco aqui a favor de uma sintaxe mais detalhada. Um dos motivos é o que você mencionou:

a sintaxe de "teclado aleatório de símbolos" pela qual o Perl é famoso

Para esclarecer, não acho que await!(f)? esteja realmente na corrida para a sintaxe final, ela foi escolhida especificamente porque é uma maneira sólida de não se comprometer com nenhuma escolha em particular. Aqui estão as sintaxes (incluindo o operador ? ) que acho que ainda estão "em execução":

  • await f?
  • await? f
  • await { f }?
  • await(f)?
  • (await f)?
  • f.await?

Ou possivelmente alguma combinação destes. O ponto é que vários deles contêm chaves para ser mais claro sobre a precedência e há muitas opções aqui - mas a intenção é que await será um operador de palavra-chave, não uma macro, na versão final ( exceto algumas mudanças importantes como a que rpjohnst propôs).

Eu voto em um simples operador de espera de postfix (por exemplo, ~ ) ou a palavra-chave sem parênteses e precedência mais alta.

Tenho lido este tópico e gostaria de propor o seguinte:

  • await f? avalia o operador ? primeiro, e então aguarda o futuro resultante.
  • (await f)? espera o futuro primeiro e, em seguida, avalia o operador ? em relação ao resultado (devido à precedência do operador Rust comum)
  • await? f está disponível como açúcar sintático para `(await f) ?. Eu acredito que "futuro retornando um resultado" será um caso supercomum, então uma sintaxe dedicada faz muito sentido.

Eu concordo com outros comentaristas que await deve ser explícito. É muito fácil fazer isso em JavaScript, e eu realmente aprecio a clareza e a legibilidade do código Rust, e sinto que tornar async implícito arruinaria isso para o código async.

Ocorreu-me que "bloco assíncrono implícito" deveria ser implementado como um proc_macro, que simplesmente insere uma palavra-chave await antes de qualquer futuro.

A questão principal é qual opção dentre as listadas você acha que se encaixa melhor na linguagem Rust atual,

Perguntar a um designer C # o que melhor se adapta à linguagem da ferrugem é ... interessante :)

Não me sinto qualificado para fazer tal determinação. Eu gosto de ferrugem e mexer com ela. Mas não é uma linguagem que eu uso todos os dias. Nem eu o arraiguei profundamente em minha psique. Como tal, não acho que esteja qualificado para fazer qualquer reclamação sobre quais são as escolhas apropriadas para este idioma aqui. Quer me perguntar sobre Go / TypeScript / C # / VB / C ++. Claro, eu me sentiria muito mais confortável. Mas a ferrugem está muito fora da minha área de especialização para me sentir confortável com tais pensamentos.

A principal consideração contra a sintaxe C # é a precedência do operador await foo?

Isso é algo que sinto que posso comentar. Nós pensamos muito sobre precedência com 'esperar' e tentamos muitos formulários antes de definir o formulário que queríamos. Uma das coisas principais que descobrimos foi que, para nós e para os clientes (internos e externos) que queriam usar esse recurso, raramente as pessoas realmente queriam 'encadear' algo além de sua chamada assíncrona. Em outras palavras, as pessoas pareciam gravitar fortemente no sentido de 'aguardar' ser a parte mais importante de qualquer expressão completa e, portanto, estar perto do topo. Nota: por 'expressão completa' quero dizer coisas como a expressão que você obtém no topo de uma instrução de expressão, ou a expressão à direita de uma atribuição de nível superior, ou a expressão que você passa como um 'argumento' para algo.

A tendência das pessoas de querer 'continuar' com o 'esperar' dentro de uma expr era rara. Ocasionalmente vemos coisas como (await expr).M() , mas parecem menos comuns e menos desejáveis ​​do que a quantidade de pessoas que fazem await expr.M() .

É também por isso que não optamos por nenhuma forma 'implícita' para 'aguardar'. Na prática, era algo sobre o qual as pessoas queriam pensar com clareza e que queriam na frente e no centro de seu código para que pudessem prestar atenção nele. Curiosamente, mesmo anos depois, essa tendência se manteve. isto é, às vezes, muitos anos depois, lamentamos que algo seja excessivamente prolixo. Alguns recursos são bons assim no início, mas uma vez que as pessoas se sintam confortáveis ​​com eles, são mais adequados com algo mais simples. Esse não foi o caso com 'esperar'. As pessoas ainda parecem realmente gostar da natureza pesada dessa palavra-chave e da precedência que escolhemos.

Até agora, estamos muito felizes com a escolha de precedência para nosso público. Podemos, no futuro, fazer algumas alterações aqui. Mas, no geral, não há forte pressão para isso.

-

bem como a diferença de que, ao contrário da execução do C #, não é executado no thread do chamador até o primeiro esperar, mas não começa, da mesma forma que o snippet de código atual não executa verificações de negatividade até que GetEnumerator seja chamado pela primeira vez:

IMO, a maneira como fizemos os enumeradores foi um tanto quanto um erro e gerou muita confusão ao longo dos anos. Tem sido especialmente ruim por causa da propensão de uma grande quantidade de código ter que ser escrito assim:

`` `c #
void SomeEnumerator (X args)
{
// Valide Args, faça trabalho síncrono.
retornar SomeEnumeratorImpl (args);
}

void SomeEnumeratorImpl (X args)
{
// ...
colheita
// ...
}

People have to write this *all the time* because of the unexpected behavior that the iterator pattern has.  I think we were worried about expensive work happening initially.  However, in practice, that doesn't seem to happen, and people def think about the work as happening when the call happens, and the yields themselves happening when you actually finally start streaming the elements.

Linq (which is the poster child for this feature) needs to do this *everywhere*, this highly diminishing this choice.

For ```await``` i think things are *much* better.  We use 'async/await' a ton ourselves, and i don't think i've ever once said "man... i wish that it wasn't running the code synchronously up to the first 'await'".  It simply makes sense given what the feature is.  The feature is literally "run the code up to await points, then 'yield', then resume once the work you're yielding on completes".  it would be super weird to not have these semantics to me since it is precisely the 'awaits' that are dictating flow, so why would anything be different prior to hitting the first await.

Also... how do things then work if you have something like this:

```c#
async Task FooAsync()
{
    if (cond)
    {
        // only await in method
        await ...
    }
} 

Você pode chamar totalmente esse método e nunca acertar em espera. se "a execução não é executada no thread do chamador antes de aguardar pela primeira vez", o que realmente acontece aqui?

aguardam? f está disponível como açúcar sintático para `(await f) ?. Eu acredito que "futuro retornando um resultado" será um caso supercomum, então uma sintaxe dedicada faz muito sentido.

Isso ressoa mais em mim. Ele permite que 'aguardar' seja o conceito superior, mas também permite o manuseio simples de tipos de resultado.

Uma coisa que sabemos do C # é que a intuição das pessoas sobre a precedência está ligada a espaços em branco. Então, se você tiver "esperar x?" então, imediatamente parece que await tem menos precedência do que ? porque ? confina com a expressão. Se o acima for realmente analisado como (await x)? isso seria uma surpresa para o nosso público.

Analisá-lo como await (x?) pareceria mais natural apenas pela sintaxe e atenderia à necessidade de obter um 'Resultado' de um futuro / tarefa de volta, e querer 'aguardar' se você realmente recebesse um valor . Se isso então retornou um Resultado por si só, parece apropriado ter isso combinado com o 'esperar' para sinalizar que isso acontece depois. então await? x? each ? se vincula fortemente à parte do código com a qual ele se relaciona mais naturalmente. O primeiro ? relacionado com await (e especificamente o resultado dele), e o segundo está relacionado com x .

se "a execução não é executada no thread do chamador antes de aguardar pela primeira vez", o que realmente acontece aqui?

Nada acontece até que o chamador espera o valor de retorno de FooAsync , altura em que FooAsync 's corpo é executado até que um await ou ele retorna.

Funciona dessa maneira porque Rust Future s são baseados em pesquisas, alocados em pilha e imóveis após a primeira chamada para poll . O chamador deve ter a chance de movê-los para o lugar - na pilha de Future s de nível superior, ou então por valor dentro de um pai Future , geralmente no "quadro de pilha" de uma chamada async fn --antes que qualquer código seja executado.

Isso significa que estamos presos a uma) semântica do tipo gerador C #, em que nenhum código é executado na invocação, ou b) semântica do tipo corrotina Kotlin, em que chamar a função também a espera imediata e implicitamente (com o tipo de fechamento async { .. } blocos de

Eu meio que prefiro o último, porque evita o problema que você mencionou com geradores C # e também evita totalmente a questão da precedência do operador.

@CyrusNajmabadi Em Rust, Future geralmente não funciona até que seja gerado como Task (é muito mais semelhante a F # Async ):

let bar = foo();

Nesse caso, foo() retorna Future , mas provavelmente não faz nada. Você deve gerá-lo manualmente (que também é semelhante a F # Async ):

tokio::run(bar);

Quando for gerado, ele executará Future . Como este é o comportamento padrão de Future , seria mais consistente para async / await no Rust não executar nenhum código até que ele fosse gerado.

Obviamente, a situação é diferente em C #, porque em C # quando você chama foo() ele imediatamente começa a executar Task , então faz sentido em C # executar o código até o primeiro await .

Além disso ... como então funcionam as coisas se você tiver algo assim [...] Você pode chamar totalmente esse método e nunca acertar em espera. se "a execução não é executada no thread do chamador antes de aguardar pela primeira vez", o que realmente acontece aqui?

Se você chamar FooAsync() , ele não fará nada, nenhum código será executado. Então, quando você o gerar, ele executará o código de forma síncrona, o await nunca será executado e, portanto, imediatamente retorna () (que é a versão de Rust de void )

Em outras palavras, não é "a execução não é executada no thread do chamador até a primeira espera", é "a execução não é executada até que seja explicitamente gerado (como com tokio::run )"

Nada acontece até que o chamador aguarde o valor de retorno de FooAsync, ponto em que o corpo de FooAsync é executado até um await ou ele retorna.

Ick. Isso parece lamentável. Muitas vezes, posso não conseguir esperar algo (muitas vezes devido ao cancelamento e composição de tarefas). Como desenvolvedor, ainda apreciaria receber esses erros antecipados (que é uma das razões mais comuns pelas quais as pessoas querem que a execução seja executada até o momento).

Isso significa que estamos presos a uma) semântica semelhante ao gerador C #, em que nenhum código é executado na invocação, ou b) à semântica semelhante à corrotina Kotlin, em que chamar a função também a espera imediata e implicitamente (com assíncrono semelhante ao fechamento {. .} blocos para quando você precisar de execução simultânea).

Diante disso, eu preferiria muito mais o primeiro do que o último. Apenas minha preferência pessoal. Se a abordagem kotlin parece mais natural para o seu domínio, vá em frente!

@CyrusNajmabadi Ick. Isso parece lamentável. Muitas vezes, posso não conseguir esperar algo (muitas vezes devido ao cancelamento e composição de tarefas). Como desenvolvedor, ainda apreciaria receber esses erros antecipados (que é uma das razões mais comuns pelas quais as pessoas querem que a execução seja executada até o momento).

Eu sinto exatamente o oposto. Na minha experiência com JavaScript, é muito comum esquecer de usar await . Nesse caso, o Promise ainda será executado, mas os erros serão engolidos (ou outras coisas estranhas acontecem).

Com o estilo Rust / Haskell / F #, o Future é executado (com o tratamento de erros correto) ou nem mesmo funciona. Então você percebe que ele não está funcionando, então você o investiga e corrige. Eu acredito que isso resulta em um código mais robusto.

@Pauan @rpjohnst Obrigado pelas explicações. Essas foram as abordagens que consideramos também. Mas acabou não sendo tão desejável na prática.

Nos casos em que você não queria "realmente fazer nada. Você tem que gerá-lo manualmente", achamos mais limpo modelar isso como o retorno de algo que gerou tarefas sob demanda. ou seja, algo tão simples quanto Func<Task> .

Eu sinto exatamente o oposto. Na minha experiência com JavaScript, é muito comum esquecer de usar o await.

C # funciona para tentar garantir que você esperou ou, de outra forma, usou a tarefa de maneira sensata.

mas os erros serão engolidos

Isso é o oposto do que estou dizendo. Estou dizendo que quero que o código seja executado rapidamente para que os erros sejam coisas que eu acerto imediatamente, mesmo no caso de eu nunca acabar executando o código na tarefa. O mesmo ocorre com os iteradores. Prefiro saber que a estava criando incorretamente no momento em que chamo a função, em vez de potencialmente muito mais adiante na linha se / quando o iterador for transmitido.

Então você percebe que ele não está funcionando, então você o investiga e corrige.

Nos cenários que estou falando, mas "não correr" é completamente razoável. Afinal, meu aplicativo pode decidir a qualquer momento que não precisa realmente executar a tarefa. Esse não é o bug que estou descrevendo. O bug que estou descrevendo é que não passei na validação e quero descobrir sobre isso o mais próximo do ponto em que criei logicamente o trabalho, em oposição ao ponto em que o trabalho realmente precisa ser executado. Dado que esses são modelos para descrever o processamento assíncrono, costuma ser o caso de eles estarem distantes um do outro. Portanto, é importante que as informações sobre os problemas aconteçam o mais cedo possível.

Conforme mencionado, isso também não é hipotético. Algo semelhante acontece com streams / iteradores. Muitas vezes as pessoas os criam, mas só os percebem mais tarde. É um fardo extra para as pessoas rastrear essas coisas de volta à sua origem. É por isso que tantas APIs (incluindo hte BCL) agora precisam fazer a divisão entre o trabalho síncrono / inicial e o trabalho adiado / preguiçoso real.

Isso é o oposto do que estou dizendo. Estou dizendo que quero que o código seja executado rapidamente para que os erros sejam coisas que eu acerto imediatamente, mesmo no caso de eu nunca acabar executando o código na tarefa.

Posso entender o desejo de erros iniciais, mas estou confuso: em que situação você "acabaria não conseguindo gerar Future "?

A maneira Future s funcionam no Rust é que você compõe Future s juntos de várias maneiras (incluindo async / await, incluindo combinadores paralelos, etc.) e, ao fazer isso, cria um único fundido Future que contém todos os sub- Future s. E então no nível superior do seu programa ( main ) você usa tokio::run (ou similar) para gerá-lo.

Além daquela única chamada tokio::run em main , você normalmente não irá gerar Future s manualmente, ao invés disso, você apenas os compõe. E a composição lida naturalmente com spawning / tratamento de erros / cancelamento / etc. corretamente.

eu também quero deixar algo bem claro. Quando eu digo algo como:

Mas acabou não sendo tão desejável na prática.

Estou falando muito especificamente sobre coisas com nossa linguagem / plataforma. Só posso dar uma ideia sobre as decisões que fizeram sentido para C # /. Net / CoreFx etc. Pode ser completamente o caso de que sua situação seja diferente e o que você deseja otimizar e os tipos de abordagens que você deve adotar de uma forma inteiramente direção diferente.

Posso entender o desejo de erros precoces, mas estou confuso: em que situação você "acabaria não conseguindo gerar o Futuro"?

O tempo todo :)

Considere como o próprio Roslyn (o compilador C # / VB / base de código IDE) é escrito. É altamente assíncrono e interativo . ou seja, o caso de uso principal é para ser usado de forma compartilhada com muitos clientes que o acessam. Os serviços do Cliest são comuns, interagindo com o usuário por meio de uma grande variedade de recursos, muitos dos quais decidem que não precisam mais fazer o trabalho que originalmente pensavam ser importante, devido ao usuário realizar uma série de ações. Por exemplo, conforme o usuário está digitando, estamos fazendo toneladas de composições e manipulações de tarefas, e podemos acabar decidindo nem mesmo executá-las porque outro evento veio alguns ms depois.

Por exemplo, conforme o usuário está digitando, estamos fazendo toneladas de composições e manipulações de tarefas, e podemos acabar decidindo nem mesmo executá-las porque outro evento veio alguns ms depois.

Mas isso não é resolvido apenas pelo cancelamento?

E a composição lida naturalmente com spawning / tratamento de erros / cancelamento / etc. corretamente.

Parece simplesmente que temos dois modelos muito diferentes para representar as coisas. Tudo bem :) Minhas explicações devem ser tomadas no contexto do modelo que escolhemos. Eles podem não fazer sentido para o modelo que você está escolhendo.

Parece simplesmente que temos dois modelos muito diferentes para representar as coisas. Tudo bem :) Minhas explicações devem ser tomadas no contexto do modelo que escolhemos. Eles podem não fazer sentido para o modelo que você está escolhendo.

Com certeza, estou apenas tentando entender sua perspectiva e também explicando nossa perspectiva. Obrigado por dedicar seu tempo para explicar as coisas.

Mas isso não é resolvido apenas pelo cancelamento?

O cancelamento é um conceito ortogonal à assincronia (para nós). Eles são comumente usados ​​juntos. Mas nenhum deles precisa do outro.

Você poderia ter um sistema inteiramente sem cancelamento, e pode simplesmente ser o caso de você nunca conseguir executar o código que "aguarda" as tarefas que você compôs. ou seja, por uma razão lógica, seu código pode apenas dizer "não preciso esperar 't', só vou fazer outra coisa". Nada sobre tarefas (em nosso mundo) dita ou exige que se espere que essa tarefa seja esperada. Em tal sistema, gostaria de obter uma validação antecipada.

Observação: isso é semelhante ao problema do iterador. Você pode ligar para alguém para obter os resultados que pretende usar mais tarde em seu código. No entanto, por uma série de razões, você pode acabar não tendo que usar os resultados. Meu desejo pessoal ainda seria obter os resultados da validação mais cedo, mesmo que tecnicamente não pudesse tê-los obtido e meu programa fosse bem-sucedido.

Acho que existem argumentos razoáveis ​​para ambas as direções. Mas minha opinião é que a abordagem síncrona tem mais prós do que contras. Claro, se a abordagem síncrona literalmente não se encaixa devido a como seu implante real deseja trabalhar, isso parece responder à pergunta sobre o que você precisa fazer: D

Em outras palavras, não acho que sua abordagem seja ruim aqui. E se ele tiver grandes benefícios em torno deste modelo que você acha que é o certo para o Rust, então, com certeza, vá em frente :)

Você poderia ter um sistema inteiramente sem cancelamento, e pode simplesmente ser o caso de você nunca conseguir executar o código que "aguarda" as tarefas que você compôs. ou seja, por uma razão lógica, seu código pode apenas dizer "não preciso esperar 't', só vou fazer outra coisa".

Pessoalmente, acho que é melhor lidar com a lógica if/then/else usual:

async fn foo() {
    if some_condition {
        await!(bar());
    }
}

Mas, como você disse, é apenas uma perspectiva muito diferente do C #.

Pessoalmente, acho que é melhor lidar com a lógica if / then / else usual:

sim. isso estaria bem se a verificação da condição pudesse ser feita no mesmo ponto em que a tarefa é criada (e muitos casos são assim). Mas em nosso mundo geralmente não é o caso de que as coisas estejam tão bem conectadas assim. Afinal, queremos fazer um trabalho assíncrono ansiosamente em resposta aos usuários (para que os resultados estejam prontos quando necessário), mas podemos decidir mais tarde que não nos importamos mais.

Em nossos domínios a 'espera' acontece no momento em que a pessoa "precisa do valor", que é uma determinação / componente / etc. Diferente. da decisão sobre "devo começar a trabalhar no valor?"

Em certo sentido, eles são muito dissociados e isso é visto como uma virtude. O produtor e o consumidor podem ter políticas totalmente diferentes, mas podem se comunicar com eficácia sobre o trabalho assíncrono que está sendo feito por meio da bela abstração da 'Tarefa'.

De qualquer forma, eu irei desistir da opinião sincronizada / assíncrona. Obviamente, existem modelos muito diferentes em jogo aqui. :)

Em termos de precedência, dei algumas informações sobre como o C # pensa sobre as coisas. Espero que seja útil. Deixe-me saber se você quiser mais informações lá.

@CyrusNajmabadi Sim, seus insights foram muito úteis. Pessoalmente, concordo com você que await? foo é o caminho a seguir (embora eu também goste da proposta "explícita async ").

Aliás, se você quiser uma das melhores opiniões de especialistas sobre todas as complexidades do modelo .net em torno da modelagem de trabalho assíncrono / sincronizado e todos os prós / contras desse sistema, @stephentoub é a pessoa com quem conversar. Ele seria cerca de 100 vezes melhor do que eu para explicar as coisas, esclarecer os prós / contras e provavelmente ser capaz de mergulhar fundo nos modelos de ambos os lados. Ele está intimamente familiarizado com a abordagem do .net aqui (incluindo as escolhas feitas e as escolhas rejeitadas) e como isso teve que evoluir desde o início. Ele também está dolorosamente ciente dos custos de desempenho das abordagens que o .net adotou (que é uma das razões pelas quais ValueTask existe agora), que eu imagino que seria algo que vocês estão pensando em primeiro lugar com seu desejo de zero / baixo -custo abstrações.

Pelo que me lembro, pensamentos semelhantes sobre essas divisões foram colocados na abordagem do .net nos primeiros dias, e acho que ele poderia falar muito bem sobre as decisões finais que foram tomadas e como elas foram apropriadas.

Eu ainda votaria a favor de await? future mesmo que pareça um pouco estranho. Existem desvantagens reais em compô-los?

Aqui está outra análise completa dos prós e contras da assíncrona fria (F #) versus quente (C #, JS): http://tomasp.net/blog/async-csharp-differences.aspx

Agora existe um novo RFC para macros postfix que permitiria a experimentação com postfix await sem uma mudança de sintaxe dedicada: https://github.com/rust-lang/rfcs/pull/2442

await {} é o meu favorito aqui, uma reminiscência de unsafe {} além de mostrar precedência.

let value = await { future }?;

@seunlanlege
sim, é remeniscente, então as pessoas têm a falsa suposição de que podem escrever códigos como este

let value = await {
   let val1 = future1;
   future2(val1)
}

Mas eles não podem.

@Pzixel
se bem entendi, você está presumindo que as pessoas presumem que os futuros são implicitamente aguardados dentro de um bloco await {} ? Eu discordo disso. await {} esperaria apenas pela expressão avaliada pelo bloco.

let value = await {
    let future = create_future();
    future
};

E deve ser um padrão desencorajado

simplificado

let value = await { create_future() };

Você propõe uma declaração em que mais de uma expressão "deve ser desencorajado". Você não vê nada de errado nisso?

É favorável fazer await um padrão (exceto ref etc)?
Algo como:

let await n = bar();

Prefiro chamar isso de padrão async do que await , embora não veja muita vantagem em torná-lo uma sintaxe de padrão. As sintaxes de padrão geralmente funcionam duplamente em relação às suas contrapartes de expressão.

De acordo com a página atual de https://doc.rust-lang.org/nightly/std/task/index.html , o mod de tarefa consiste em reexportações de libcore e reexportações para liballoc, o que torna o resultado um pouco ... subótimo. Espero que isso seja resolvido de alguma forma antes de se estabilizar.

Dei uma olhada no código. E eu tenho algumas sugestões:

  • [x] O traço UnsafePoll e Poll enum têm nomes muito semelhantes, mas não estão relacionados. Eu sugiro renomear UnsafePoll , por exemplo, UnsafeTask .
  • [x] Na caixa de futuros, o código foi dividido em diferentes submódulos. Agora, a maior parte do código está agrupada em task.rs que torna a navegação mais difícil. Eu sugiro dividir novamente.
  • [x] TaskObj#from_poll_task() tem um nome estranho. Eu sugiro nomeá-lo new() vez disso
  • [x] TaskObj#poll_task poderia ser apenas poll() . O campo denominado poll poderia ser denominado poll_fn que também sugere que é um ponteiro de função
  • Waker pode ser capaz de usar a mesma estratégia de TaskObj e colocar a vtable na pilha. Só uma ideia, não sei se queremos isso. Seria mais rápido porque é um pouco menos indireto?
  • [] dyn agora está estável em beta. O código provavelmente deve usar dyn onde se aplica

Também posso fornecer um RP para essas coisas. @cramertj @aturon sinta-se à vontade para

que tal apenas adicionar um método await() para todos os Future ?

    /// just like and_then method
    let x = f.and_then(....);
    let x = f.await();

    await f?     =>   f()?.await()
    await? f     =>   f().await()?

/// with chain invoke.
let x = first().await().second().await()?.third().await()?
let x = first().await()?.second().await()?.third().await()?
let x = first()?.await()?.second().await()?.third().await()?

@zengsai O problema é que await não funciona como um método normal. Na verdade, considere o que o método await faria quando não estivesse em um bloco / função async . Os métodos não sabem em que contexto são executados, portanto, isso não poderia causar erro de compilação.

@xfix isso não é verdade em geral. O compilador pode fazer o que quiser e pode lidar com a chamada do método especialmente neste caso. A chamada de estilo de método resolve o problema de preferência, mas é inesperado (await não funciona dessa maneira em outras linguagens) e provavelmente seria um hack feio no compilador.

@elszben Que o compilador pode fazer o que quiser, não significa que deva fazer o que quiser.

future.await() soa como uma chamada de função normal, embora não seja. Se você quiser seguir esse caminho, a sintaxe future.await!() proposta em algum lugar acima permitiria a mesma semântica e marcaria claramente com uma macro “Algo estranho está acontecendo aqui, eu sei”.

Editar: postagem removida

Mudei este post para a RFC de futuros. Link

Alguém viu a interação entre async fn e #[must_use] ?

Se você tiver um async fn , chamá-lo diretamente não executa nenhum código e retorna um Future ; parece que tudo async fn deve ter uma inerente #[must_use] no "exterior" impl Future tipo, então você não pode chamá-los sem fazer algo com o Future .

Além disso, se você anexar #[must_use] a async fn você mesmo, parece que isso se aplica ao retorno da função interna . Portanto, se você escrever #[must_use] async fn foo() -> T { ... } , não poderá escrever await!(foo()) sem fazer algo com o resultado de await.

Alguém viu a interação entre fn assíncrono e # [must_use]?

Para outros interessados ​​nesta discussão, consulte https://github.com/rust-lang/rust/issues/51560.

Eu estava pensando em como as funções assíncronas são implementadas e percebi que essas funções não suportam recursão, ou recursão mútua também.

para a sintaxe de await, estou pessoalmente voltado para as macros post-fix, nenhuma abordagem de await implícita, por seu encadeamento fácil e que também pode ser usado como uma chamada de método

@ warlord500 você está ignorando completamente toda a experiência de milhões de desenvolvedores descritos acima. Você não quer encadear await 's.

@Pzixel, por favor, não presuma que não li o tópico ou o que quero.
Eu sei que algum colaborador pode não querer permitir o encadeamento de espera, mas há alguns de nós
desenvolvedores que o fazem. Não tenho certeza de onde você tirou a noção de que eu estava ignorando
opiniões dos desenvolvedores, meu comentário apenas especificou uma opinião de um membro da comunidade e minhas razões para ter essa opinião.

EDITAR : se você tem uma diferença de opinião, por favor, compartilhe! Estou curioso para saber por que você diz
não devemos permitir o encadeamento de espera por meio de um método como a sintaxe?

@ warlord500 porque a equipe MS compartilhou sua experiência com milhares de clientes e milhões de desenvolvedores. Eu mesmo sei porque escrevo código assíncrono / aguardo no dia-a-dia e você nunca quer encadea-los. Aqui está a citação exata, se desejar:

Nós pensamos muito sobre precedência com 'esperar' e tentamos muitos formulários antes de definir o formulário que queríamos. Uma das coisas principais que descobrimos foi que, para nós e para os clientes (internos e externos) que queriam usar esse recurso, raramente as pessoas realmente queriam 'encadear' algo além de sua chamada assíncrona. Em outras palavras, as pessoas pareciam gravitar fortemente no sentido de 'aguardar' ser a parte mais importante de qualquer expressão completa e, portanto, estar perto do topo. Nota: por 'expressão completa' quero dizer coisas como a expressão que você obtém no topo de uma instrução de expressão, ou a expressão à direita de uma atribuição de nível superior, ou a expressão que você passa como um 'argumento' para algo.

A tendência das pessoas de querer 'continuar' com o 'esperar' dentro de uma expr era rara. Ocasionalmente vemos coisas como (await expr).M() , mas parecem menos comuns e menos desejáveis ​​do que a quantidade de pessoas que fazem await expr.M() .

Agora estou bastante confuso, se bem entendi, não deveríamos apoiar
aguarde o estilo de cadeia fácil pós-correção porque não é comumente usado? você vê esperar como a parte mais importante de uma expressão.
Eu apenas presumo, neste caso, para ter certeza de que entendi corretamente.
Se eu estiver errado, não hesite em me corrigir.

Além disso, você pode postar o link de onde obteve a citação,
obrigado.

Minha contrariedade aos dois pontos acima é apenas porque você não usa algo comumente, não significa necessariamente que apoiá-lo seria prejudicial no caso de tornar o código mais limpo.

às vezes, aguardam na parte mais importante de uma expressão, se a futura expressão geradora for
a parte mais importante e você gostaria de colocá-la no topo, você ainda pode fazer isso se permitíssemos um estilo de macro postfix em adição ao estilo de macro normal

Além disso, você pode postar o link de onde obteve a citação,
obrigado.

Mas ... mas você disse que leu todo o tópico ... 😃

Mas não tenho nenhum problema em compartilhá-lo: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886. Eu sugiro que você leia todos os posts do Cyrus, é realmente a experiência de todo o ecossistema C # / .Net, é uma experiência inestimável que pode ser reutilizada pelo Rust.

às vezes aguardam na parte mais importante de uma expressão

A citação diz claramente o oposto 😄 E você sabe, eu também tenho o mesmo sentimento, escrevendo assíncrono / aguardando no dia-a-dia.

Você tem experiência com async / await? Você pode compartilhar então, por favor?

Uau, não posso acreditar que perdi isso. Obrigado por dedicar um tempo do seu dia para vincular isso.
Eu não tenho nenhuma experiência, então, acho que no grande esquema as coisas minha opinião não importa muito

@Pzixel Agradeço por compartilhar informações sobre a sua experiência e a de outras pessoas usando async / await , mas seja respeitoso com os outros contribuidores. Você não precisa criticar o nível de experiência dos outros para fazer com que seus próprios pontos técnicos sejam ouvidos.

Nota do moderador: @Pzixel Ataques pessoais a membros da comunidade não são permitidos. Eu editei fora do seu comentário. Não faça isso de novo. Se você tiver dúvidas sobre nossa política de moderação, entre em contato conosco em [email protected].

@crabtw Não critiquei ninguém itt. Peço desculpas por qualquer inconveniente que possa ter ocorrido aqui.

Eu perguntei sobre a experiência uma vez quando eu queria entender se a pessoa tem uma necessidade real de encadear 'await's ou se é a extrapolação dos recursos de hoje. Eu não queria apelar para a autoridade, era apenas um monte de informações úteis onde posso dizer "você precisa tentar por si mesmo e perceber essa verdade por si mesmo". Nada ofensivo aqui.

Ataques pessoais a membros da comunidade não são permitidos. Eu editei fora do seu comentário.

Sem ataques pessoais. Como posso ver, você comentou minha referência sobre votos negativos. Bem, foi apenas o meu reator na minha votação negativa, nada de especial. Como ela foi removida, também é razoável remover essa referência (pode até ser confuso para outros leitores), então, obrigado por removê-la.

Obrigado pela referência. Eu queria mencionar que você não deve tomar nada do que eu digo como 'gospel' :) Rust e C # são linguagens diferentes com comunidades, paradigmas e idiomas diferentes. Você deve definitivamente fazer as melhores escolhas para o seu idioma. Espero que minhas palavras sejam úteis e possam dar uma visão. Mas sempre esteja aberto a maneiras diferentes de fazer as coisas.

Minha esperança é que você encontre algo incrível para Rust. Então, podemos ver o que você fez e roubar graciosamente, adotá-lo para C # :)

Pelo que eu posso dizer, o argumento vinculado fala principalmente sobre a precedência de await e, em particular, argumenta que faz sentido analisar await x.y() como await (x.y()) vez de (await x).y() porque o usuário irá querer e esperar mais frequentemente a primeira interpretação (e o espaçamento também sugere essa interpretação). E eu tenderia a concordar, embora também observasse que a sintaxe como await!(x.y()) remove a ambigüidade.

No entanto, não acho que isso sugira qualquer resposta particular em relação ao valor do encadeamento, como x.y().await!().z() .

O comentário citado é interessante em parte porque há uma grande diferença no Rust, que tem sido um dos grandes fatores para atrasar nossa descoberta da sintaxe de espera final: C # não tem operador ? , então eles não têm código que precisaria ser escrito (await expr)? . Eles descrevem (await expr).M() como realmente incomum, e eu tendo a pensar que isso seria verdade também em Rust, mas a única exceção a isso, da minha perspectiva, é ? , que será muito comum porque muitos futuros avaliarão os resultados ( todos os que existem agora o fazem, por exemplo).

@sem embarcações sim, isso mesmo. Eu gostaria de citar esta parte mais uma vez:

a única exceção a isso, da minha perspectiva, é?

Se houver apenas uma exceção, então parece razoável criar await? foo como um atalho para (await foo)? e ter o melhor dos dois mundos.

No momento, pelo menos, a sintaxe proposta de await!() permitirá o uso inequívoco de ? . Podemos nos preocupar com alguma sintaxe mais curta para a combinação de await e ? se e quando decidirmos mudar a sintaxe base para await . (E dependendo do que mudá-lo para, talvez não tenhamos um problema em tudo.)

@joshtriplett esses

Matching lines: 139 Matching files: 10 Total files searched: 77

Eu tenho 139 espera em 2743 sloc. Talvez não seja grande coisa, mas acho que devemos considerar uma alternativa sem braçadeira como mais limpa e melhor. Sendo dito, ? é a única exceção, então poderíamos facilmente usar await foo sem colchetes e introduzir uma sintaxe especial apenas para este caso especial. Não é grande coisa, mas pode economizar alguns colchetes para um projeto LISP.

Criei uma postagem de blog sobre por que acho que as funções assíncronas devem usar a abordagem de tipo de retorno externo para sua assinatura. Gostar de ler!

https://github.com/MajorBreakfast/rust-blog/blob/master/posts/2018-06-19-outer-return-type-approach.md

Não acompanhei todas as discussões, então sinta-se à vontade para apontar onde isso já teria sido discutido se eu tivesse perdido.

Aqui está uma preocupação adicional sobre a abordagem do tipo de retorno interno: como seria a sintaxe para Stream s, quando ela será especificada? Eu acharia que async fn foo() -> impl Stream<Item = T> ficaria bem e consistente com async fn foo() -> impl Future<Output = T> , mas não funcionaria com a abordagem de tipo de retorno interno. E não acho que vamos querer introduzir uma palavra-chave async_stream .

@Ekleog Stream precisaria usar uma palavra-chave diferente. Não pode usar async porque impl Trait funciona ao contrário. Ele só pode garantir que certos traços sejam implementados, mas os próprios traços precisam já estar implementados no tipo de concreto subjacente.

A abordagem do tipo de retorno externo, no entanto, seria útil se um dia quisermos adicionar funções geradoras assíncronas:

async_gen fn foo() -> impl AsyncGenerator<Yield = i32, Return = ()> { yield 1; ... }

Stream pode ser implementado para todos os geradores assíncronos com Return = () . Isso torna isso possível:

async_gen fn foo() -> impl Stream<Item = i32> { yield 1;  ... }

Observação: os geradores já funcionam à noite, mas não usam essa sintaxe. Eles também não estão atentos à fixação, ao contrário de Stream em futuros 0,3.

Editar: Este código usava anteriormente um Generator . Perdi a diferença entre Stream e Generator . Os fluxos são assíncronos. Isso significa que eles podem, mas não precisam, render um valor. Eles podem responder com Poll::Ready ou Poll::Pending . Por outro lado, Generator deve sempre render ou completar de forma síncrona. Agora mudei para AsyncGenerator para refletir isso.

Edit2: @Ekleog A implementação atual de geradores usa uma sintaxe sem marcador e parece detectar que deveria ser um gerador procurando por yield dentro do corpo. Isso significa que você estaria correto ao dizer que async poderia ser reutilizado. Se essa abordagem é sensata é outra questão, no entanto. Mas eu acho que isso é para outro assunto ^^ '

Na verdade, eu estava pensando que async poderia ser reutilizado, seria apenas porque async , de acordo com este RFC, só seria permitido com Future s e, portanto, poderia detectar ele está gerando Stream observando o tipo de retorno (que deve ser Future ou Stream ).

A razão pela qual estou levantando isso agora é porque se quisermos ter a mesma palavra-chave async para gerar Future s e Stream s, então acho que o retorno externo abordagem de tipo seria muito mais limpa, porque seria explícita, e não acho que alguém esperaria que um async fn foo() -> i32 gerasse um fluxo de i32 (o que seria possível se o corpo contivesse a yield e a abordagem do tipo de retorno interno foi escolhida).

Poderíamos ter uma segunda palavra-chave para geradores (por exemplo, gen fn ) e, em seguida, criar fluxos apenas aplicando ambos (por exemplo, async gen fn ). O tipo de retorno externo não precisa entrar nisso de forma alguma.

@rpjohnst Eu

Não queremos definir dois tipos associados. Um fluxo ainda é apenas um único tipo, não impl Iterator<Item=impl Future>> ou algo parecido.

@rpjohnst eu quis dizer os tipos associados Yield e Return de geradores (assíncronos)

gen fn foo() -> impl Generator<Yield = i32, Return = ()> { ... }

Este era o meu esboço original, mas acho que falar sobre geradores está ficando muito à frente de nós mesmos, pelo menos para o problema de rastreamento:

// generator
fn foo() -> T yields Y

// generator that implements Iterator
fn foo() yields Y

// async generator
async fn foo() -> T yields Y

// async generator that implements Stream
async fn foo() yields Y

De maneira mais geral, acho que devemos ter mais experiência com a implementação antes de revisitarmos quaisquer decisões tomadas na RFC. Estamos circulando em torno dos mesmos argumentos que já fizemos, precisamos de experiência com o recurso proposto pela RFC para ver se uma reponderação é necessária.

Eu gostaria de concordar totalmente com você, mas gostaria de saber: se eu li corretamente seu comentário, a estabilização da sintaxe async / await irá esperar por uma sintaxe e implementação decentes para streams assíncronos e para ganhar experiência com os dois? (uma vez que não seria possível alterar entre os tipos de retorno externos e os tipos de retorno internos, uma vez que esteja estabilizado)

Achei que async / await era esperado para o Rust 2018 e não esperava que os geradores assíncronos estivessem prontos até lá, mas ...?

(Além disso, meu comentário pretendia apenas ser um argumento adicional para a postagem do blog de @MajorBreakfasts , mas parece ter apagado completamente a discussão sobre este tópico ... esse não era de forma alguma o meu objetivo, e acho que o debate deveria ser centralizado em esta postagem do blog?)

O caso de uso restrito da palavra-chave await ainda me confunde. (Esp. Futuro vs Fluxo vs Gerador)

Uma palavra-chave de rendimento não seria suficiente para todos os casos de uso? Como em

{ let a = yield future; println(a) } -> Future

O que mantém o tipo de retorno explícito e, portanto, apenas uma palavra-chave é necessária para todas as semânticas baseadas em "continuação" sem fundir a palavra-chave e a biblioteca com muita força.

(Fizemos isso na língua de argila aliás)

@aep await não produz um futuro do gerador - ele pausa a execução de Future e retorna o controle ao chamador.

@cramertj bem, poderia ter feito exatamente isso embora (retornar um futuro que contém a continuação após a palavra-chave yield), que é um caso de uso muito mais amplo.
mas acho que estou um pouco atrasado para a festa dessa discussão. :)

@aep O raciocínio para uma palavra-chave específica de await é para composição com uma palavra-chave específica do gerador futuro yield . Queremos oferecer suporte a geradores assíncronos e isso significa dois "escopos" de continuação independentes.

Além disso, ele não pode retornar um futuro que contenha a continuação, porque os futuros do Rust são baseados em pesquisas e não em retorno de chamada, pelo menos parcialmente por motivos de gerenciamento de memória. Muito mais fácil para poll transformar um único objeto do que yield lançar referências a ele.

Acho que async / await não deve ser uma palavra-chave para poluir a linguagem em si, porque async é apenas um recurso não interno da linguagem.

@sackery Faz parte da parte interna da linguagem e não pode ser implementado puramente como uma biblioteca.

portanto, apenas torne-a uma palavra-chave, assim como nim, c # faz!

Pergunta: qual deve ser a assinatura de fechamentos sem movimento async que capturam valores por referência mutável? Atualmente, eles estão simplesmente banidos. Parece que queremos algum tipo de abordagem GAT que permita que o empréstimo do fechamento dure até que o futuro esteja morto, por exemplo:

trait AsyncFnMut {
    type Output<'a>: Future;
    fn call(&'a mut self, args: ...) -> Self::Output<'a>;
}

@cramertj há um problema geral aqui com o retorno de referências mutáveis ​​ao ambiente capturado de um encerramento. Possivelmente, a solução não precisa estar vinculada a fn assíncrono?

@sem barcos , certo, só vai ser muito mais comum em async situações do que provavelmente seria em outro lugar.

Que tal fn async vez de async fn ?
Eu gosto mais de let mut que mut let .

fn foo1() {
}
fn async foo2() {
}
pub fn foo3() {
}
pub fn async foo4() {
}

Depois de pesquisar pub fn , você ainda pode encontrar todas as funções públicas no código-fonte.mas atualmente a sintaxe não é.

fn foo1() {
}
async fn foo2() {
}
pub fn foo3() {
}
pub async fn foo4() {
}

Essa proposta não é muito importante, é uma questão de gosto pessoal.
Portanto, eu respeito a opinião de todos vocês :)

Acredito que todos os modificadores devem vir antes de fn. É claro e como é feito em outras línguas. É apenas um bom senso.

@Pzixel Eu sei que os modificadores de acesso devem vir antes de fn porque é importante.
mas acho que async provavelmente não é.

@xmeta Eu não vi essa ideia proposta antes. Provavelmente, queremos colocar async antes de fn para ser consistente, mas acho importante considerar todas as opções. Obrigado por publicar!

// Status quo:
pub unsafe async fn foo() {} // #![feature(async_await, futures_api)]
pub const unsafe fn foo2() {} // #![feature(const_fn)]

@MajorBreakfasts Obrigado pela sua resposta, pensei assim.

{ Public, Private } ⊇ Function  → put `pub` in front of `fn`
{ Public, Private } ⊇ Struct    → put `pub` in front of `struct`
{ Public, Private } ⊇ Trait     → put `pub` in front of `trait`
{ Public, Private } ⊇ Enum      → put `pub` in front of `enum`
Function ⊇ {Async, Sync}        → put `async` in back of `fn`
Variable ⊇ {Mutable, Imutable}  → put `mut` in back of `let`

@xmeta @MajorBreakfasts

async fn é indivisível, representa uma função assíncrona。

async fn é um todo.

Você pesquisa pub fn , isso significa que você está procurando por uma função de sincronização pública.
Da mesma forma, você pesquisa pub async fn , o que significa que você está pesquisando uma função assíncrona pública.

@ZhangHanDong

  • async fn define uma função normal que retorna um futuro. Todas as funções que retornam um futuro são consideradas "assíncronas". Os ponteiros de função de async fn outras funções que retornam um futuro são idênticos. Aqui está um exemplo de playground . Uma pesquisa por "fn assíncrono" só pode localizar as funções que usam a notação, ela não encontrará todas as funções assíncronas.
  • Uma pesquisa por pub fn não encontrará funções unsafe ou const .

° O tipo concreto retornado por async fn é obviamente anônimo. Quero dizer que ambos retornam um tipo que implementa Future

@xmeta note que mut não "vai atrás de deixar", ou melhor, que mut não modifica let . let pega um padrão, que é

let PATTERN = EXPRESSION;

mut faz parte do PATTERN , não do let si. Por exemplo:

// one is mutable one is not
let (mut a, b) = (1, 2);

@steveklabnik eu entendo. Eu só queria mostrar a associação entre a estrutura hierárquica e a ordem das palavras. Obrigada

Qual é a opinião das pessoas sobre o comportamento desejado de return e break dentro dos blocos de async ? Atualmente return retorna do bloco assíncrono - se permitirmos return todo, esta é realmente a única opção possível. Poderíamos banir return e usar algo como 'label: async { .... break 'label x; } para retornar de um bloco assíncrono. Isso também está relacionado à conversa sobre o uso da palavra-chave break ou return para o recurso de quebra para blocos (https://github.com/rust-lang/rust/issues/ 48594).

Eu sou a favor de permitir return . A principal preocupação em proibir isso é que pode ser confuso porque não retorna da função atual, mas do bloco assíncrono. Eu, no entanto, duvido que seja confuso. Fechamentos já permitem return e eu nunca achei isso confuso. Aprender que return se aplica a blocos assíncronos é IMO fácil e permitir isso é IMO bastante valioso.

@cramertj return deve sempre sair da função que o contém, nunca um bloco interno; se não fizer sentido que funcione, o que parece que não funciona, então return não deve funcionar.

Usar break para isso parece lamentável, mas como infelizmente temos o valor de quebra de rótulo, é pelo menos consistente com isso.

Movimentos assíncronos e fechamentos ainda estão planejados? O seguinte é do RFC:

// closure which is evaluated immediately
async move {
     // asynchronous portion of the function
}

e mais abaixo na página

async { /* body */ }

// is equivalent to

(async || { /* body */ })()

o que torna return alinhado com fechamentos e parece bastante fácil de entender e explicar.

O rfc break-to-block está planejando permitir o salto de um fechamento interno com um rótulo? Do contrário (e não estou sugerindo que deva permitir), seria muito lamentável não permitir o comportamento consistente de returns e, em seguida, usar uma alternativa que também seja inconsistente com rfc de quebra para blocos.

@memoryruins async || { ... return x; ... } deve funcionar perfeitamente . Estou dizendo que async { ... return x; ... } não deveria, precisamente porque async não é um encerramento. return tem um significado muito específico: "retornar da função que o contém". Os fechamentos são uma função. blocos assíncronos não.

@memoryruins Ambos já foram implementados.

@joshtriplett

blocos assíncronos não.

Acho que ainda penso neles como funções no sentido de que são um corpo com um contexto de execução definido separadamente do bloco que os contém, então faz sentido para mim que return seja interno ao async Bloco de || e async do.

@cramertj "sintática" é importante, entretanto.

Pense nisso desta maneira. Se você tem algo que não se parece com uma função (ou com um encerramento, e está acostumado a reconhecer os encerramentos como funções) e vê um return , para onde seu analisador mental acha que vai?

Qualquer coisa que sequestra return torna mais confuso ler o código de outra pessoa. As pessoas estão pelo menos acostumadas com a idéia de que break retorna para algum bloco pai e elas terão que ler o contexto para saber qual bloco. return sempre foi o maior martelo que retorna de toda a função.

Se eles não estão sendo tratados de forma semelhante aos encerramentos avaliados imediatamente, concordo que o retorno seria inconsistente, especialmente sintaticamente. Se ? em blocos assíncronos já foi decidido (a RFC ainda diz que estava indeciso), então imagino que estaria alinhado com isso.

@joshtriplett , parece arbitrário para mim dizer que você pode reconhecer funções e fechamentos (que são sintaticamente muito diferentes) como "escopos de retorno", mas blocos assíncronos não podem ser reconhecidos ao longo das mesmas linhas. Por que duas formas sintáticas distintas são aceitáveis, mas não três?

Houve alguma discussão anterior sobre este tópico na RFC . Como eu disse lá, sou a favor dos blocos assíncronos usando break _sem_ ter que fornecer um rótulo (não há como quebrar o bloco assíncrono para um loop externo para que você não perca a expressividade).

@withoutboats Um encerramento é apenas outro tipo de função; depois de aprender "um fechamento é uma função", você pode aplicar tudo o que sabe sobre funções aos fechamentos, incluindo " return sempre retorna da função que o contém".

@ Nemo157 Mesmo se você não break alvo do bloco async , você teria que fornecer um mecanismo (como 'label: async ) para retornar antecipadamente de um loop dentro de um bloco assíncrono .

@joshtriplett

Um fechamento é apenas outro tipo de função; depois de aprender "um encerramento é uma função", você pode aplicar tudo o que sabe sobre funções aos encerramentos, incluindo "retornar sempre retorna da função que o contém".

Eu acho que async blocos também são um tipo de "função" - sem argumentos que podem ser executados de forma assíncrona. Eles são um caso especial de async encerramentos que não têm argumentos e foram pré-aplicados.

@cramertj sim, eu estava assumindo que qualquer ponto de interrupção implícito também pode ser rotulado, se necessário (como acredito que todos podem atualmente).

Qualquer coisa que torne o fluxo de controle mais difícil de seguir e, em particular, redefina o que return significa, coloca uma grande pressão na capacidade de ler o código sem problemas.

Na mesma linha, a orientação padrão em C é "não escreva macros que retornem do meio da macro". Ou, como um caso menos comum, mas ainda problemático: se você escrever uma macro que se parece com um loop, break e continue devem funcionar de dentro dela. Já vi pessoas escreverem macros tipo loop que incorporam dois loops, então break não funciona como esperado, e isso é extremamente confuso.

Acho que os blocos assíncronos também são um tipo de "função"

Acho que é uma perspectiva baseada em conhecer os aspectos internos da implementação.

Não os vejo como funções de forma alguma.

Não os vejo como funções de forma alguma.

@joshtriplett

Minha suspeita é que você teria feito o mesmo argumento chegando a uma linguagem com encerramentos pela primeira vez - que return não deveria funcionar dentro do encerramento, mas dentro da função de definição. E, de fato, existem linguagens que aceitam essa interpretação, como Scala.

@cramertj Eu não, não; para lambdas e / ou funções definidas dentro de uma função, parece completamente natural que eles sejam uma função. (Minha primeira exposição a eles foi em Python, FWIW, onde lambdas não podem usar return e em funções aninhadas return retorna da função contendo return .)

Acho que, uma vez que se saiba o que um bloco assíncrono faz, fica intuitivamente claro como return deve se comportar. Depois de saber que ele representa uma execução atrasada, fica claro que return não pode se aplicar à função. É claro que a função já terá retornado quando o bloco for executado. Aprender isso pela IMO não deve ser um grande desafio. Devíamos pelo menos experimentar e ver.

Este RFC não propõe como o ? -operator e as construções de fluxo de controle como return , break e continue devem funcionar dentro de blocos assíncronos.

Seria melhor proibir quaisquer operadores de fluxo de controle ou adiar bloqueios até que um RFC dedicado seja escrito? Havia outros recursos desejados mencionados para serem discutidos mais tarde. Nesse ínterim, teremos funções assíncronas, encerramentos e await! :)

Eu concordo com @memoryruins aqui, acho que valeria a pena criar outro RFC para discutir esses detalhes em mais detalhes.

O que você acha de uma função que nos permite acessar o contexto de dentro de uma fn assíncrona, talvez chamada core::task::context() ? Ele simplesmente entraria em pânico se chamado de fora de um fn assíncrono. Acho que seria muito útil, por exemplo, acessar o executor para gerar algo.

@MajorBreakfasts essa função é chamada lazy

async fn foo() -> i32 {
    await!(lazy(|ctx| {
        // do something with ctx
        42
    }))
}

Para algo mais específico, como a desova, provavelmente haverá funções auxiliares que o tornam mais ergonômico

async fn foo() -> i32 {
    let some_task = lazy(|_| 5);
    let spawned_task = await!(spawn_with_handle(some_task));
    await!(spawned_task)
}

@ Nemo157 Na verdade, spawn_with_handle é onde eu gostaria de usar isso. Ao converter o código para 0,3, percebi que spawn_with_handle é na verdade apenas um futuro porque precisa de acesso ao contexto ( consulte o código ). O que eu gostaria de fazer é adicionar um método spawn_with_handle a ContextExt e tornar spawn_with_handle uma função gratuita que só funciona dentro de funções assíncronas:

fn poll(self: PinMut<Self>, cx: &mut Context) -> Poll<Self::Output> {
     let join_handle = ctx.spawn_with_handle(future);
     ...
}
async fn foo() {
   let join_handle = spawn_with_handle(future); // This would use this function internally
   await!(join_handle);
}

Isso removeria todas as bobagens de espera dupla que temos atualmente.

Pensando bem, o método precisaria ser chamado de core::task::with_current_context() e funcionar de maneira um pouco diferente porque deve ser impossível armazenar uma referência.

Editar: Esta função já existe com o nome get_task_cx . Ele está atualmente em libstd por motivos técnicos. Proponho torná-lo público API, uma vez que pode ser colocado no libcore.

Duvido que seja possível ter uma função que possa ser chamada a partir de uma função não async que poderia fornecer o contexto de alguma função async pai uma vez que ela tenha sido removida do TLS. Nesse ponto, o contexto provavelmente será tratado como uma variável local oculta dentro da função async , então você poderia ter uma macro que acessa o contexto diretamente nessa função, mas não haveria como ter spawn_with_handle puxa magicamente o contexto de seu chamador.

Então, potencialmente algo como

fn spawn_with_handle(executor: &mut Executor, future: impl Future) { ... }

async fn foo() {
    let join_handle = spawn_with_handle(async_context!().executor(), future);
    await!(join_handle);
}

@ Nemo157 Acho que você está certo: uma função como a que estou propondo provavelmente não funcionaria se não fosse chamada diretamente de um fn assíncrono. Talvez a melhor maneira seja fazer spawn_with_handle uma macro que usa await internamente (como select! e join! ):

async fn foo() {
    let join_handle = spawn_with_handle!(future);
    await!(join_handle);
}

Isso parece bom e pode ser implementado facilmente por meio de await!(lazy(|ctx| { ... })) dentro da macro.

async_context!() é problemático porque não pode me impedir de armazenar a referência de contexto entre os pontos de espera.

async_context!() é problemático porque não pode me impedir de armazenar a referência de contexto entre os pontos de espera.

Dependendo da implementação, pode. Se os argumentos do gerador completo fossem ressuscitados, eles precisariam ser limitados de forma que você não pudesse manter uma referência no ponto de rendimento de qualquer maneira, o valor por trás do argumento teria uma vida útil que só funcionaria até o ponto de rendimento. Async / await apenas herdaria essa limitação.

@ Nemo157 Você quer dizer algo assim?

let my_arg = yield; // my_arg lives until next yield

@Pzixel Desculpe despertar uma discussão _possivelmente_ antiga, mas gostaria de acrescentar minhas ideias.

Sim, eu gosto que a sintaxe await!() remova a ambigüidade ao combiná-la com coisas como ? , mas também concordo que essa sintaxe é chata de digitar milhares de vezes em um único projeto. Também acredito que é barulhento e que um código limpo é importante.

É por isso que estou me perguntando qual é o verdadeiro argumento contra um símbolo com sufixo (que foi mencionado algumas vezes antes), como something_async()@ comparar com algo com await , talvez porque await é uma palavra-chave bem conhecida em outros idiomas? O @ pode ser engraçado, pois se assemelha a um a de await, mas pode ser qualquer símbolo que se encaixe perfeitamente.

Eu diria que tal escolha de sintaxe seria lógica, já que algo semelhante aconteceu com try!() que basicamente se tornou um sufixo ? (eu sei que não é exatamente o mesmo). É conciso, fácil de lembrar e fácil de digitar.

Outra coisa incrível sobre essa sintaxe é que o comportamento é imediatamente claro quando combinado com o símbolo ? (pelo menos eu acredito que seria). Dê uma olhada no seguinte:

// Await, then unwrap a Result from the future
awaiting_a_result()@?;

// Unwrap a future from a result, then await
result_with_future()?@;

// The real crazy can make it as funky as they want
magic()?@@?@??@; 
// - I'm joking, of course

Isso não tem o problema de await future? , quando não está claro à primeira vista o que acontecerá a menos que você saiba sobre tal situação. E ainda assim, sua implementação é consistente com ? .

Agora, existem apenas algumas coisas _pequenas_ que posso pensar que iriam contra essa ideia:

  • talvez seja _muito_ conciso e menos visível / detalhado ao contrário de algo com await , o que torna _difícil_ localizar pontos de suspensão em uma função.
  • talvez seja assimétrico com a palavra-chave async , onde uma é uma palavra-chave e a outra um símbolo. Embora await!() sofra do mesmo problema que é um keywore versus uma macro.
  • escolher um símbolo adiciona outro elemento sintaxial e algo a aprender. Mas, supondo que isso possa se tornar algo comumente usado, não acho que isso seja um problema.

@phaux também mencionou o uso do símbolo ~ . No entanto, eu acredito que esse personagem é funky para digitar em alguns layouts de teclado, portanto, eu recomendo abandonar essa ideia.

Quais são seus pensamentos pessoal? Você concorda que é de alguma forma semelhante a como try!() _became_ ? ? Você prefere await ou um símbolo e por quê? Estou louco por discutir isso ou talvez esteja faltando alguma coisa?

Desculpe por qualquer terminologia incorreta que eu possa ter usado.

A maior preocupação que tenho com a sintaxe baseada em sigilos é que ela pode facilmente se transformar em sopa de glifos, como você gentilmente demonstrou. Qualquer pessoa familiarizada com Perl (pré-6) entenderá onde estou indo com isso. Evitar o ruído da linha é a melhor maneira de seguir em frente.

Dito isso, talvez o melhor caminho a seguir seja exatamente como try! ? Ou seja, comece com uma macro async!(foo) explícita e, se necessário, adicione algum sigilo que seria um açúcar para ela. Claro, isso está adiando o problema para mais tarde, mas async!(foo) é o caminho suficiente para uma primeira iteração de async / await, com a vantagem de ser relativamente não controverso. (e de ter o precedente de try! / ? deve surgir de um sigilo)

@withoutboats Não li este tópico inteiro, mas alguém está ajudando na implementação? Onde está o seu ramo de desenvolvimento?

E, em relação às considerações restantes não resolvidas, alguém pediu ajuda de especialistas fora da comunidade Rust? Joe Duffy conhece e se preocupa muito com a simultaneidade e entende os detalhes complicados muito bem , e ele deu uma palestra na RustConf , então eu suspeito que ele pode ser receptivo a um pedido de orientação, se tal pedido for garantido.

@BatmanAoD, uma implementação inicial foi realizada em https://github.com/rust-lang/rust/pull/51580

O thread RFC original teve comentários de vários especialistas no espaço PLT, fora do Rust mesmo :)

Gostaria de sugerir o símbolo '$' para aguardar Futuros, porque tempo é dinheiro, e quero lembrar isso ao compilador.

Apenas brincando. Não acho uma boa ideia ter um símbolo de espera. Rust significa ser explícito e permitir que as pessoas escrevam códigos de baixo nível em uma linguagem poderosa que não permite que você dê um tiro no próprio pé. Um símbolo é muito mais vago do que uma macro await! e permite que as pessoas atiram no próprio pé de uma maneira diferente, escrevendo um código difícil de ler. Eu já diria que ? é um passo longe demais.

Também discordo da existência de uma palavra-chave async para ser usada na forma async fn . Isso implica em algum tipo de "tendência" para o assíncrono. Por que async merece uma palavra-chave? O código assíncrono é apenas outra abstração para nós que nem sempre é necessária. Acho que um atributo assíncrono atua mais como uma "extensão" do dialeto Rust básico, que nos permite escrever um código mais poderoso.

Não sou um arquiteto de linguagem, mas tenho um pouco de experiência em escrever código assíncrono com Promises em JavaScript, e acho que a maneira como isso é feito torna um prazer escrever código assíncrono.

@steveklabnik Ah, ok, obrigado. Podemos (/ devo) atualizar a descrição do problema? Talvez o item de marcador "implementação inicial" deva ser dividido em "implementação sem move suporte" e "implementação completa"?

A próxima iteração de implementação está sendo trabalhada em alguma bifurcação / ramificação pública? Ou isso não pode continuar até que o RFC 2418 seja aceito?

Por que o problema de sintaxe async / await está sendo discutido aqui, e não em um RFC?

@ c-edw Acho que a pergunta sobre a palavra-chave async foi respondida por Qual a cor da sua função?

@parasyte Foi-me sugerido que essa postagem é, na verdade, um argumento contra toda a ideia de funções assíncronas sem simultaneidade de estilo de thread verde gerenciada automaticamente.

Eu discordo dessa posição, porque threads verdes não podem ser implementados de forma transparente sem um tempo de execução (gerenciado), e há um bom motivo para o Rust oferecer suporte a código assíncrono sem exigir isso.

Mas parece que a sua leitura da postagem é que a semântica async / await está bem, mas há uma conclusão a ser tirada sobre a palavra-chave? Você se importaria de expandir isso?

Eu concordo com seu ponto de vista também. Eu estava comentando que a palavra-chave async é necessária e o artigo expõe o raciocínio por trás dela. As conclusões tiradas pelo autor são um assunto diferente.

@parasyte Ah, ok. Que bom que perguntei - por causa da aversão do autor às dicotomias vermelho / azul, pensei que você estivesse dizendo o contrário!

Gostaria de esclarecer melhor, pois sinto que não fiz justiça.

A dicotomia é inevitável . Alguns projetos tentaram apagá-lo tornando cada chamada de função assíncrona, garantindo que as chamadas de função de sincronização não existissem. Midori é um exemplo óbvio. E outros projetos tentaram apagar a dicotomia ocultando as funções assíncronas por trás da fachada das funções de sincronização. gevent é um exemplo desse tipo.

Ambos têm o mesmo problema; eles ainda precisam da dicotomia para distinguir entre aguardar a conclusão de uma tarefa assíncrona e

  • Midori introduziu não apenas a palavra-chave await , mas também uma palavra-chave async no site de chamada de função .
  • gevent fornece gevent.spawn além da espera implícita de chamadas de função de aparência normal.

Esse foi o motivo pelo qual trouxe à baila o artigo color-a-function, já que ele responde à pergunta: "Por que async merece uma palavra-chave?"

Bem, mesmo o código síncrono baseado em thread pode distinguir "esperando a conclusão de uma tarefa" (junção) e "iniciando uma tarefa" (geração). Você poderia imaginar uma linguagem em que tudo é assíncrono (em termos de implementação), mas não há nenhuma anotação em await (porque é o comportamento padrão) e o async Midori é, em vez disso, um encerramento passado para um spawn API. Isso coloca tudo assíncrono exatamente na mesma base sintática / cor de função que o sincronismo total.

Portanto, embora concorde que async merece uma palavra-chave, parece-me que é mais porque Rust se preocupa com o mecanismo de implementação neste nível e precisa fornecer as duas cores por esse motivo.

@rpjohnst Sim, li suas propostas. Conceitualmente, é o mesmo que ocultar as cores à la gevent. Que eu critiquei no fórum ferrugem no mesmo tópico; cada chamada de função parece síncrona, o que é um perigo particular quando uma função é síncrona e bloqueada em um pipeline assíncrono. Esse tipo de bug é imprevisível e um verdadeiro desastre para solucionar.

Não estou falando da minha proposta em particular, estou falando de uma linguagem onde tudo é assíncrono. Você pode escapar da dicotomia dessa forma - minha proposta não tenta isso.

IIUC foi exatamente o que Midori tentou. Nesse ponto, palavras-chave versus fechamentos é apenas uma discussão semântica.

Em quinta-feira, 12 de julho de 2018 às 15h01, Russell Johnston [email protected]
escreveu:

Você usou a presença de palavras-chave como argumento para explicar por que a dicotomia
ainda existe em Midori. Se você os remover, onde está a dicotomia? O
sintaxe é idêntica ao código all-sync, mas com os recursos de assíncrono
código.

Porque quando você chama uma função assíncrona sem esperar seu resultado, ela
de forma síncrona retorna uma promessa. O que pode ser aguardado mais tarde. 😐

_ Nossa, alguém sabe alguma coisa sobre a Midori? Eu sempre pensei que era um projeto fechado com quase nenhuma criatura viva trabalhando nele. Seria interessante se alguém de vocês tivesse escrito sobre isso com mais detalhes._

/fora do assunto

@Pzixel Nenhuma criatura viva ainda está trabalhando nisso, porque o projeto foi encerrado. Mas o blog de Joe Duffy tem muitos detalhes interessantes. Veja meus links acima.

Saímos dos trilhos aqui e sinto que estou me repetindo, mas isso é parte da "presença de palavras-chave" - ​​a palavra-chave await . Se você substituir as palavras-chave por APIs como spawn e join , poderá ser totalmente assíncrono (como Midori), mas sem qualquer dicotomia (ao contrário de Midori).

Ou seja, como disse antes, não é fundamental - só o temos porque queremos a escolha.

@CyrusNajmabadi desculpe por pingar você novamente, mas aqui estão algumas informações adicionais sobre a tomada de decisão.

Se você não quiser ser mencionado novamente, diga-me, por favor. Eu apenas pensei que você pudesse estar interessado.

Do canal de discórdia # wg-net :

@cramertj
alimento para o pensamento: frequentemente escrevo Ok::<(), MyErrorType>(()) no final de async { ... } blocos. talvez haja algo que possamos propor para tornar mais fácil restringir o tipo de erro.

@withoutboats
[...] possivelmente queremos que seja consistente com [ try ]?

(Lembro-me de uma discussão relativamente recente sobre como blocos try poderiam declarar seu tipo de retorno, mas não consigo encontrar agora ...)

matizes mencionados:

async -> io::Result<()> {
    ...
}

async: io::Result<()> {
    ...
}

async as io::Result<()> {
    ...
}

Uma coisa que try pode fazer e que é menos ergonômica com async é usar uma associação de variável ou atribuição de tipo, por exemplo

let _: io::Result<()> = try { ... };
let _: impl Future<Output = io::Result<()>> = async { ... };

Eu já havia discutido anteriormente a ideia de permitir sintaxe semelhante a fn para o traço Future , por exemplo, Future -> io::Result<()> . Isso faria com que a opção de fornecimento de tipo manual parecesse um pouco melhor, embora ainda tenha muitos caracteres:

let _: impl Future -> io::Result<()> = async {
}
async -> impl Future<Output = io::Result<()>> {
    ...
}

seria minha escolha.

É semelhante à sintaxe de encerramento existente:

|x: i32| -> i32 { x + 1 };

Editar: E eventualmente, quando for possível para TryFuture implementar Future :

async -> impl TryFuture<Ok = i32, Error = ()> {
    ...
}

Edit2: Para ser preciso, o acima funcionaria com as definições de características de hoje. Acontece que um tipo TryFuture não é tão útil hoje porque atualmente não implementa Future

@MajorBreakfast Por que -> impl Future<Output = io::Result<()>> vez de -> io::Result<()> ? Nós já fazemos a remoção do tipo de retorno para async fn foo() -> io::Result<()> , então IMO se usarmos uma sintaxe baseada em -> , parece claro que queremos o mesmo açúcar aqui.

@cramertj Sim, deve ser consistente. Minha postagem acima meio que presume que posso convencer todos vocês de que a abordagem do tipo de retorno externo é superior 😁

No caso de usarmos async -> R { .. } então presumivelmente também devemos usar try -> R { .. } , bem como usar expr -> TheType em geral para atribuição de tipo. Em outras palavras, a sintaxe de atribuição de tipo que usamos deve ser aplicada uniformemente em todos os lugares.

@Centril eu concordo. Deve ser usado em qualquer lugar. Só não tenho mais certeza se -> é realmente a escolha certa. Eu associo -> a ser exigível. E os blocos assíncronos não são.

@MajorBreakfasts Eu basicamente concordo; Acho que devemos usar : para atribuição de tipo, então async : Type { .. } , try : Type { .. } e expr : Type . Discutimos as ambigüidades em potencial no Discord e acho que encontramos uma maneira de avançar com : que faz sentido ...

Outra pergunta é sobre Either enum. Já temos Either em futures engradado. Também é confuso porque se parece com Either de either engradado quando não é.

Porque Futures parece estar mesclado em std (pelo menos as partes muito básicas dele), poderíamos também incluir Either lá? É crucial tê-los para poder retornar impl Future da função.

Por exemplo, costumo escrever código como o seguinte:

fn handler() -> impl Future<Item = (), Error = Bar> + Send {
    someFuture()
        .and_then(|x| {
            if condition(&x) {
                Either::A(anotherFuture(x))
            } else {
                Either::B(future::ok(()))
            }
        })
}

Eu gostaria de poder escrever assim:

async fn handler() -> Result<(), Bar> {
    let x = await someFuture();
    if condition(&x) {
        await anotherFuture(x);
    }
}

Mas, pelo que entendi, quando async é expandido, é necessário que Either seja inserido aqui, porque ou entramos em condição ou não.

_Você pode encontrar o código real aqui, se desejar.

@Pzixel, você não precisará de Either dentro de async funções, contanto que você await os futuros, a transformação de código que async faz ocultará esses dois tipos internamente e apresentam um único tipo de retorno para o compilador.

@Pzixel Além disso, (pessoalmente) espero que Either não seja introduzido com este RFC, porque isso apresentaria uma versão restrita de https://github.com/rust-lang/rfcs/issues / 2414 (que funciona apenas com 2 tipos e apenas com Future s), provavelmente adicionando fragmentos de API se uma solução geral for fundida - e como @ Nemo157 mencionou, não parece ser uma emergência para tem Either agora :)

@Ekleog , claro, fui atingido pela ideia de que, na verdade, tenho toneladas de either no meu código assíncrono existente e gostaria muito de me livrar deles. Então me lembrei de minha confusão quando gastei cerca de meia hora até perceber que ele não compila porque eu tinha either engradado em dependências (erros futuros são muito difíceis de entender, então demorou muito). Portanto, é por isso que escrevi o comentário, apenas para ter certeza de que esse problema seja resolvido de alguma forma.

Claro, isso não está relacionado apenas a async/await , é uma coisa mais genérica, então merece seu próprio RFC. Eu só queria enfatizar que futures deve saber sobre either ou vice-versa (para implementar IntoFuture corretamente).

@Pzixel O Either exportado pela caixa de futuros é uma reexportação da caixa either . O futures crate 0.3 não pode implementar Future para Either por causa das regras órfãs. É muito provável que também vamos remover os impls Stream e Sink por Either para consistência e oferecer uma alternativa (discutida aqui ). Além disso, a caixa either poderia então implementar Future , Stream e Sink si, provavelmente sob um sinalizador de recurso.

Dito isso, como @ Nemo157 já mencionou, ao trabalhar com futuros, é melhor usar apenas funções assíncronas em vez de Either .

O material async : Type { .. } agora é proposto em https://github.com/rust-lang/rfcs/pull/2522.

As funções assíncronas / aguardar que implementam Send automaticamente já foram implementadas?

Parece que a seguinte função assíncrona não é (ainda?) Send :

pub async fn __receive() -> ()
{
    let mut chan: futures::channel::mpsc::Receiver<Box<Send + 'static>> = None.unwrap();

    await!(chan.next());
}

O link para o reprodutor completo (que não compila no playground por falta de futures-0.3 , eu acho) está aqui .

Além disso, ao investigar esse problema, encontrei https://github.com/rust-lang/rust/issues/53249, que acho que deve ser adicionado à lista de rastreamento da postagem superior :)

Aqui está um playground mostrando que as funções async / await implementando Send _should_ funcionam. Descomentar a versão Rc detecta corretamente essa função como diferente de Send . Posso dar uma olhada em seu exemplo específico daqui a pouco (sem o compilador Rust nesta máquina: ligeiramente_frowning_face :) para tentar descobrir por que ele não está funcionando.

@Ekleog std::mpsc::Receiver não é Sync , e o async fn você escreveu contém uma referência a ele. Referências a !Sync itens são !Send .

@cramertj Hmm… mas não estou segurando um mpsc::Receiver , que deveria ser Send se seu tipo genérico for Send ? (também, não é std::mpsc::Receiver mas futures::channel::mpsc::Receiver , que também é Sync se o tipo for Send , desculpe por não notar o mpsc::Receiver alias era ambíguo!)

@ Nemo157 Obrigado! Abri https://github.com/rust-lang/rust/issues/53259 para evitar muito barulho sobre este problema :)

A questão de se e como os blocos async permitem ? e outro fluxo de controle pode justificar alguma interação com os blocos try (por exemplo, try async { .. } para permitir ? sem confusão semelhante a return ?).

Isso significa que o mecanismo para especificar um tipo de bloco async pode precisar interagir com o mecanismo para especificar um tipo de bloco try . Deixei um comentário sobre a sintaxe de atribuição RFC: https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Basta atingir o que a princípio pensei ser um problema de futures-rs , mas acabou que pode ser um problema assíncrono / aguardar, então aqui está: https://github.com/rust-lang-nursery/ futures-rs / issues / 1199 # issuecomment -413089012

Conforme discutido há alguns dias no Discord, await ainda não foi reservado como uma palavra-chave. É muito importante obter esta reserva (e adicionada ao lint de palavra-chave da edição de 2018) antes do lançamento de 2018. É uma reserva um pouco complicada, pois queremos continuar usando a sintaxe macro por enquanto.

A API Future / Task terá uma maneira de gerar futuros locais?
Vejo que há SpawnLocalObjError , mas parece não ser usado.

@panicbit No grupo de trabalho, estamos discutindo se faz sentido incluir a funcionalidade de spawning no contexto. https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError não está totalmente sem uso: LocalPool da caixa de futuros usa. Você está correto, entretanto, que nada na libcore usa)

@withoutboats Notei que alguns dos links na descrição do problema estão desatualizados. Especificamente, https://github.com/rust-lang/rfcs/pull/2418 foi fechado e https://github.com/rust-lang-nursery/futures-rs/issues/1199 foi movido para https: / /github.com/rust-lang/rust/issues/53548

NB. O nome desse problema de rastreamento é assíncrono / aguardar, mas também é atribuído à API de tarefa! A API de tarefa atualmente tem um RFC de estabilização pendente: https://github.com/rust-lang/rfcs/pull/2592

Alguma chance de tornar as palavras-chave reutilizáveis ​​para implementações assíncronas alternativas? atualmente, ele cria um Futuro, mas é uma espécie de oportunidade perdida de tornar o assíncrono baseado em push mais utilizável.

@aep É possível converter facilmente de um sistema baseado em push no sistema Future baseado em pull usando oneshot::channel .

Como exemplo, JavaScript Promises são baseados em push, então stdweb usa oneshot::channel para converter JavaScript Promises em Rust Futures . Ele também usa oneshot::channel para algumas outras APIs de retorno de chamada baseadas em push, como setTimeout .

Por causa do modelo de memória de Rust, Futures baseados em push têm custos de desempenho extras em comparação com pull . Portanto, é melhor pagar esse custo de desempenho apenas quando necessário (por exemplo, usando oneshot::channel ), em vez de ter todo o sistema Future baseado em push.

Dito isso, não faço parte das equipes principais ou de idiomas, então nada do que eu diga tem autoridade. É apenas minha opinião pessoal.

na verdade, é o contrário no código de recursos limitados. os modelos pull têm uma penalidade porque você precisa do recurso dentro do que está sendo puxado, em vez de alimentar o próximo valor pronto por meio de uma pilha de funções de espera. O design do futures.rs é simplesmente caro demais para qualquer coisa próxima ao hardware, como switches de rede (meu caso de uso) ou renderizadores de jogos.

No entanto, neste caso, tudo o que precisamos aqui é fazer com que async emita algo como o Generator faz. Como eu disse antes, acho que async e generators são na verdade a mesma coisa se você abstraí-los o suficiente em vez de vincular duas palavras-chave a uma única biblioteca.

No entanto, neste caso, tudo o que precisamos aqui é fazer com que async emita algo como o Generator faz.

async neste ponto é literalmente um envoltório mínimo em torno de um gerador literal. Estou tendo dificuldade em ver como os geradores ajudam com E / S assíncrona baseada em push. Você não precisa de uma transformação CPS para eles?

Você poderia ser mais específico sobre o que entende por "você precisa dos recursos dentro do que está sendo puxado?" Não sei por que você precisaria disso, ou como "alimentar o próximo valor pronto por meio de uma pilha de funções de espera" é diferente de poll() .

Fiquei com a impressão de que os futuros baseados em push eram mais caros (e, portanto, mais difíceis de usar em ambientes restritos). Permitir que retornos de chamada arbitrários sejam anexados a um futuro requer alguma forma de indireção, normalmente alocação de heap, portanto, em vez de alocar uma vez com o futuro raiz, você aloca em cada combinador. E o cancelamento também se torna mais complexo devido a problemas de segurança de thread, então você não oferece suporte ou exige que todas as conclusões de retorno de chamada usem operações atômicas para evitar corrida. Tudo isso resulta em uma estrutura muito mais difícil de otimizar, pelo que eu posso dizer.

você não precisa de uma transformação CPS para eles?

sim, a sintaxe do gerador atual não funciona para isso porque não tem argumentos para a continuação, e é por isso que eu esperava que o async trouxesse maneiras de fazê-lo.

você precisa dos recursos dentro do que está sendo puxado?

essa é a minha maneira terrível de dizer que inverter a ordem assíncrona funciona duas vezes tem custo. Ou seja, uma vez do hardware para o futuro e vice-versa, usando os canais. Você precisa carregar um monte de coisas que não trazem nenhum benefício em código próximo ao hardware.

Um exemplo comum seria que você não pode simplesmente invocar a pilha futura quando sabe que um descritor de arquivo de um soquete está pronto, mas em vez disso, deve implementar toda a lógica de execução para mapear eventos do mundo real para futuros, o que tem custo externo, como bloqueio, tamanho do código e, mais importante, complexidade do código.

Permitir que retornos de chamada arbitrários sejam anexados a um futuro requer alguma forma de indireção

sim, eu entendo que os retornos de chamada são caros em alguns ambientes (não no meu, onde a velocidade de execução é irrelevante, mas eu tenho 1 MB de memória total, então o futures.rs nem cabe no flash), no entanto, você não precisa de despacho dinâmico quando tem algo como continuações (que o conceito do gerador atual meio-implementa).

E o cancelamento também se torna mais complexo devido à segurança do thread

acho que estamos confundindo as coisas aqui. Não estou defendendo chamadas de retorno. Continuações podem ser pilhas estáticas. Por exemplo, o que implementamos na linguagem clay é apenas um padrão gerador que você pode usar para empurrar ou puxar. ie:

async fn add (a: u32) -> u32 {
    let b = await
    a + b
}

add(3).continue(2) == 5

Acho que posso continuar fazendo isso com uma macro, mas sinto que é uma oportunidade perdida aqui, desperdiçando uma palavra-chave em um conceito específico.

não no meu, onde a velocidade de execução é irrelevante, mas eu tenho 1 MB de memória total, então futures.rs nem cabe no flash

Tenho certeza de que os futuros atuais se destinam a ser executados em ambientes com restrição de memória. O que exatamente está ocupando tanto espaço?

Editar: este programa ocupa 295 KB de espaço em disco quando compilado --release no meu macbook (hello world básico leva 273 KB):

use futures::{executor::LocalPool, future};

fn main() {
    let mut pool = LocalPool::new();
    let hello = pool.run_until(future::ready("Hello, world!"));
    println!("{}", hello);
}

não no meu, onde a velocidade de execução é irrelevante, mas eu tenho 1 MB de memória total, então futures.rs nem cabe no flash

Tenho certeza de que os futuros atuais se destinam a ser executados em ambientes com restrição de memória. O que exatamente está ocupando tanto espaço?

Além disso, o que você quer dizer com memória? Eu executei o código atual baseado em assíncrono / espera em dispositivos com flash de 128 kB / RAM de 16 kB. Definitivamente, existem problemas de uso de memória com async / await atualmente, mas esses são principalmente problemas de implementação e podem ser melhorados adicionando algumas otimizações adicionais (por exemplo, https://github.com/rust-lang/rust/issues/52924).

Um exemplo comum seria que você não pode simplesmente invocar a pilha futura quando sabe que um descritor de arquivo de um soquete está pronto, mas em vez disso, deve implementar toda a lógica de execução para mapear eventos do mundo real para futuros, o que tem custo externo, como bloqueio, tamanho do código e, mais importante, complexidade do código.

Por quê? Isso ainda não parece algo a que o futuro o force. Você pode chamar poll tão facilmente quanto faria com um mecanismo baseado em push.

Além disso, o que você quer dizer com memória?

Não acho isso relevante. Toda essa discussão acabou invalidando um ponto que eu nem mesmo tinha a intenção de fazer. Não estou aqui para criticar o futuro, além de dizer que fixar seu design na linguagem central é um erro.

Meu ponto é que a palavra-chave async pode ser feita à prova de futuro se for feita corretamente. Continuações é o que eu quero, mas talvez outras pessoas tenham ideias ainda melhores.

Você pode chamar a votação com a mesma facilidade com que faria um mecanismo baseado em push.

Sim, faria sentido se Future: poll tivesse call args. Não pode tê-los porque a pesquisa precisa ser abstrata. Em vez disso, estou propondo emitir uma continuação da palavra-chave assíncrona e implantar Future para qualquer continuação com zero argumentos.

É uma alteração simples e de baixo esforço que não adiciona nenhum custo aos futuros, mas permite a reutilização de palavras-chave que atualmente são exclusivas para uma biblioteca.

Mas é claro que as continations podem ser implementadas com um pré-processador, que é o que vamos fazer. Infelizmente, o desugar pode ser apenas um fechamento, que é mais caro do que uma continuação adequada.

@aep Como poderíamos reutilizar as palavras-chave ( async e await )?

@Centril minha solução rápida ingênua seria reduzir um assíncrono para um gerador e não para um futuro. Isso dará tempo para tornar o gerador útil para continuações adequadas, em vez de ser um back-end exclusivo para futuros.

É como um PR de 10 linhas talvez. Mas eu não tenho paciência para lutar contra uma colmeia de abelhas por isso, vou apenas construir um pré-processo para desugar uma palavra-chave diferente.

Eu não tenho seguido o material assíncrono, então peço desculpas se isso foi discutido antes / em outro lugar, mas qual é o plano (de implementação) para suportar async / await em no_std ?

AFAICT a implementação atual usa TLS para passar um Waker, mas não há suporte TLS (ou thread) em no_std / core . Ouvi de @alexcrichton que pode ser possível se livrar do TLS se / quando Generator.resume ganhar suporte para argumentos.

O plano para bloquear a estabilização do assíncrono / esperar no suporte no_std está sendo implementado? Ou temos certeza de que o suporte no_std pode ser adicionado sem alterar nenhuma das peças que serão estabilizadas para enviar std async / await no stable?

@japaric poll agora leva o contexto explicitamente. AFAIK, TLS não deve mais ser necessário.

https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll

Editar: não é relevante para assíncrono / esperar, apenas para futuros.

[...] temos certeza de que o suporte no_std pode ser adicionado sem alterar nenhuma das peças que serão estabilizadas para enviar std async / await no stable?

Eu acredito que sim. As partes relevantes são as funções em std::future , todas elas estão escondidas atrás de um recurso instável gen_future que nunca será estabilizado. A transformação async usa set_task_waker para armazenar o waker em TLS então await! usa poll_with_tls_waker para obter acesso a ele. Se os geradores obtiverem suporte ao argumento de retomada, em vez disso, a transformação async pode passar o waker como o argumento de retomada e await! pode lê-lo fora do argumento.

EDIT: Mesmo sem argumentos do gerador, acredito que isso também poderia ser feito com algum código um pouco mais complicado na transformação assíncrona. Eu pessoalmente gostaria de ver os argumentos do gerador adicionados para outros casos de uso, mas tenho certeza de que será possível remover o requisito de TLS com / sem eles.

@japaric Mesmo barco. Mesmo que alguém faça futuros funcionarem em embutidos, é muito arriscado, já que é tudo Tier3.

Eu descobri um hack feio que requer muito menos trabalho do que consertar async: weave in an Arcatravés de uma pilha de Geradores.

  1. veja o argumento "Poll" https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 é um Arc
  2. registrando algo na enquete na Linha 87
  3. rendimento para gerar um ponto de continuação na linha 92
  4. chame um gerador de um gerador para criar uma pilha de nível superior na linha 207
  5. finalmente executando toda a pilha passando em um tempo de execução na linha 215

O ideal é que eles reduzam o assíncrono para uma pilha de encerramento "pura" em vez de um Future, de forma que você não precise de nenhuma suposição de tempo de execução e possa inserir o ambiente impuro como um argumento na raiz.

Eu estava no meio do caminho para implementar isso

https://twitter.com/arvidep/status/1067383652206690307

mas meio inútil ir até o fim se eu sou o único que quer.

E eu não conseguia parar de pensar se assíncrono sem TLS / espera sem argumentos de gerador é possível, então implementei um par de macro no_std proc-macro com base em async_block! / await! usando apenas variáveis ​​locais.

Definitivamente, requer garantias de segurança muito mais sutis do que a solução atual baseada em TLS ou uma solução baseada em argumento de gerador (pelo menos quando você assume que os argumentos do gerador subjacentes são válidos), mas tenho certeza de que é válida (contanto que ninguém usa o buraco de higiene bastante grande que não consegui encontrar uma maneira de contornar, isso não seria um problema para uma implementação no compilador, pois pode usar identificadores gensym inomináveis ​​para se comunicar entre a transformação assíncrona e a macro de espera).

Acabei de perceber que não há menção de mover await! de std para core no OP, talvez # 56767 possa ser adicionado à lista de problemas a serem resolvidos antes da estabilização para rastrear isto.

@ Nemo157 Como await! não deve ser estabilizado, ele não é um bloqueador de qualquer maneira.

@Centril Não sei quem te disse que await! não deve ser estabilizado ...: wink:

@cramertj Ele quis dizer a versão macro, não a versão da palavra-chave, eu acredito ...

@ crlf0710 e quanto à versão de bloco assíncrono implícito / espera / explícita ?

@ crlf0710 Eu também :)

@cramertj Não queremos remover a macro porque há atualmente um hack feio no compilador que torna possível a existência de await e await! ? Se estabilizarmos a macro, nunca seremos capazes de removê-la.

@stjepang Eu realmente não me importo muito em qualquer direção com a sintaxe de await! , além de uma preferência geral por notações pós-fixadas e uma aversão à ambigüidade e símbolos impronunciáveis ​​/ impossíveis de serem habilitados pelo Google. Pelo que eu sei, as sugestões atuais (com ? para esclarecer a precedência) são:

  • await!(x)? (o que temos hoje)
  • await x? ( await liga-se mais firmemente do que ? , ainda notação de prefixo, precisa de parênteses para métodos em cadeia)
  • await {x}? (o mesmo que acima, mas requer temporariamente {} para eliminar a ambigüidade)
  • await? x ( await liga-se com menos força, ainda prefixa notação, precisa de parênteses para métodos em cadeia)
  • x.await? (parece um acesso de campo)
  • x# / x~ / etc. (algum símbolo)
  • x.await!()? (postfix-macro-style, @withoutboats e eu acho que talvez outros não sejam fãs de postfix-macros porque eles esperam que . permita o envio baseado em tipo, o que não faria para macros postfix )

Acho que o melhor caminho para o envio é chegar a await!(x) , sem palavra-chave await e, eventualmente, algum dia vender às pessoas a gentileza das macros postfix, permitindo-nos adicionar x.await!() . Outras pessoas têm opiniões diferentes;)

Eu sigo essa questão de maneira muito vaga, mas aqui está minha opinião:

Pessoalmente, gosto da macro await! como ela é e como está descrita aqui: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Não é nenhum tipo de mágica ou nova sintaxe, apenas uma macro regular. Afinal, menos é mais.

Então, novamente, eu também preferi try! , já que Try ainda não está estabilizado. No entanto, await!(x)? é um meio-termo decente entre sugar e ações nomeadas óbvias, e acho que funciona bem. Além disso, ele poderia ser potencialmente substituído por alguma outra macro em uma biblioteca de terceiros para lidar com funcionalidades extras, como rastreamento de depuração.

Enquanto isso async / yield é "apenas" açúcar sintático para geradores. Isso me lembra dos dias em que o JavaScript estava recebendo suporte assíncrono / aguardar e você tinha projetos como Babel e Regenerator que transpilaram código assíncrono para usar geradores e Promises / Futures para operações assíncronas, essencialmente como estamos fazendo.

Tenha em mente que, eventualmente, desejaremos assíncronos e geradores como recursos distintos, potencialmente até combináveis ​​entre si (produzindo um Stream ). Deixar await! como uma macro que apenas diminui para yield não é uma solução permanente.

Saindo, aguarde! já que uma macro que apenas diminui para produzir não é uma solução permanente.

Não pode ser permanentemente visível para o usuário que desça para yield , mas certamente pode continuar a ser implementado dessa forma. Mesmo quando você tem assíncronos + geradores = Stream você ainda pode usar, por exemplo, yield Poll::Pending; vs. yield Poll::Ready(next_value) .

Lembre-se de que, eventualmente, queremos assíncronos e geradores como recursos distintos

Async e geradores não são recursos distintos? Relacionado, é claro, mas comparando isso novamente com como o JavaScript fazia, sempre pensei que async seria construído sobre geradores; que a única diferença sendo uma função assíncrona retornaria e produziria Future s em oposição a qualquer valor regular. Um executor seria necessário para avaliar e aguardar a execução da função assíncrona. Além de algumas coisas extras para a vida, não tenho certeza.

Na verdade, uma vez escrevi uma biblioteca sobre isso, avaliando recursivamente funções assíncronas e funções geradoras que retornavam Promessas / Futuros.

@cramertj Não pode ser implementado dessa forma se os dois forem "efeitos" distintos. Há alguma discussão sobre isso aqui: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. Não queremos yield Poll::Ready(next_value) , queremos yield next_value e ter await s em outro lugar na mesma função.

@rpjohnst

Não queremos gerar Poll :: Ready (next_value), queremos gerar next_value e ter espera em outro lugar na mesma função.

Sim, é claro que é o que pareceria para o usuário, mas em termos de desavença você só tem que embrulhar yield s em Poll::Ready e adicionar Poll::Pending a o yield gerado a partir de await! . Sintaticamente para os usuários finais, eles aparecem como recursos separados, mas ainda podem compartilhar uma implementação no compilador.

@cramertj Também este:

  • await? x

@novacrazy Sim, eles são características distintas, mas eles devem estar juntos combináveis.

E, de fato, em JavaScript, eles são combináveis:

https://thenewstack.io/whats-coming-up-in-javascript-2018-async-generators-better-regex/

“Geradores e iteradores assíncronos são o que você obtém quando combina uma função assíncrona e um iterador, então é como um gerador assíncrono que você pode esperar ou uma função assíncrona da qual você pode lucrar”, explicou ele. Anteriormente, o ECMAScript permitia que você escrevesse uma função que você poderia ceder ou esperar, mas não ambas. “Isso é realmente conveniente para consumir streams que estão se tornando cada vez mais parte da plataforma da web, especialmente com o objeto Fetch expondo streams.”

O iterador assíncrono é semelhante ao padrão Observable, mas mais flexível. “Um Observable é um modelo push; depois de assiná-lo, você recebe uma explosão de eventos e notificações em alta velocidade, esteja pronto ou não, portanto, é necessário implementar estratégias de buffer ou amostragem para lidar com a tagarelice ”, explicou Terlson. O iterador assíncrono é um modelo push-pull - você pede um valor e ele é enviado a você - que funciona melhor para coisas como primitivas de IO de rede.

@Centril ok, aberto # 56974, isso é correto o suficiente para ser adicionado como uma questão não resolvida ao OP?


Eu realmente não quero entrar na sintaxe de await bicicleta novamente, mas tenho que responder a pelo menos um ponto:

Pessoalmente, gosto da macro await! como ela é e como está descrita aqui: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Observe que eu também disse que não acredito que a macro possa permanecer uma macro implementada pela biblioteca (ignorando se ela continuará ou não a aparecer como uma macro para os usuários), para expandir os motivos:

  1. Escondendo a implementação subjacente, como diz um dos problemas não resolvidos, você pode criar um gerador usando || await!() .
  2. O suporte a geradores assíncronos, como @cramertj menciona, requer a diferenciação entre os yield s adicionados por await e outros yield s escritos pelo usuário. Isso _pode_ ser feito como um estágio de pré-expansão da macro, _ se_ os usuários nunca quiseram yield dentro das macros, mas existem yield -in-macro construções muito úteis como yield_from! . Com a restrição de que yield s em macros devem ser suportados, isso requer que await! seja pelo menos uma macro embutida (se não for a sintaxe real).
  3. Apoiando async fn em no_std . Eu conheço duas maneiras de implementar isso, ambas exigem que async fn -criado- Future e await compartilhem um identificador no qual o waker está armazenado. A única maneira eu pode ver para ter um identificador higienicamente seguro compartilhado entre esses dois locais se ambos forem implementados no compilador.

Eu acho que há um pouco de confusão aqui - nunca foi a intenção que await! fosse publicamente expansível visivelmente para um invólucro em torno de chamadas para yield . Qualquer futuro para a sintaxe semelhante à macro await! dependerá de uma implementação não diferente daquela do atual compile_error! , assert! , format_args! compile_error! , assert! , format_args! etc. e seria capaz de desugar em um código diferente, dependendo do contexto.

A única parte importante a entender aqui é que não há uma diferença semântica significativa entre qualquer uma das sintaxes propostas - elas são apenas sintaxe de superfície.

Eu escreveria uma alternativa para resolver a sintaxe await .

Em primeiro lugar, gosto da ideia de colocar await como um operador postfix. Mas expression.await é muito parecido com um campo, como já apontado.

Portanto, minha proposta é expression awaited . A desvantagem aqui é que awaited ainda não foi preservado como uma palavra-chave, mas é mais natural em inglês e ainda assim não existem tais expressões (quero dizer, formas gramaticais como expression [token] ) são válidas em Rust agora, então isso pode ser justificado.

Então podemos escrever expression? awaited para aguardar Result<Future,_> , e expression awaited? para aguardar Future<Item=Result<_,_>> .

@earthengine

Embora eu não esteja convencido da palavra-chave awaited , acho que você está no caminho certo.

O ponto-chave aqui é: yield e await são como return e ? .

return x retorna o valor x , enquanto x? desembrulha o resultado x , retornando mais cedo se for Err .
yield x produz o valor x , enquanto x awaited aguarda o futuro x , retornando mais cedo se for Pending .

Há uma boa simetria nisso. Talvez await realmente deva ser um operador Postfix.

let x = x.do_something() await.do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Não sou fã de uma sintaxe aguardada de postfix pelo motivo exato que @cramertj acabou de mostrar. Ele reduz a legibilidade geral, especialmente para expressões longas ou expressões encadeadas. Não dá a sensação de aninhamento como await! / await faria. Não tem a simplicidade de ? , e estamos ficando sem símbolos para usar para um operador Postfix ...

Pessoalmente, ainda sou a favor de await! pelos motivos que descrevi anteriormente. Parece enferrujado e sem sentido.

Ele reduz a legibilidade geral, especialmente para expressões longas ou expressões encadeadas.

Nos padrões Rustfmt, o exemplo deve ser escrito

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;

Mal posso ver como isso afeta a legibilidade.

Eu gosto de Postfix Aguardar também. Acho que usar um espaço seria incomum e tenderia a quebrar o agrupamento mental. No entanto, eu acho que .await!() seria par bem, com ? montagem antes ou depois, e o ! permitiria interações controle de fluxo.

(Isso não requer um mecanismo de macro postfix totalmente geral; o compilador poderia ser um caso especial de .await!() .)

Eu comecei realmente não gostando do postfix await (sem . ou () ), pois parece muito estranho - pessoas que vêm de outras línguas vão rir muito de nosso despesa com certeza. Esse é um custo que devemos levar a sério. No entanto, x await claramente não é uma chamada de função ou um acesso de campo ( x.await / x.await() / await(x) todos têm este problema) e há menos funky questões de precedência. Esta sintaxe resolveria claramente ? e precedência de acesso de método, por exemplo, foo await? e foo? await ambos têm ordem de precedência clara para mim, assim como foo await?.x e foo await?.y (não negando que eles parecem estranhos, apenas argumentando que a precedência é clara).

eu também acho que

stream.for_each(async |item| {
    ...
}) await;

lê mais bem do que

await!(stream.for_each(async |item| {
    ...
});

Em suma, eu apoiaria isso.

@joshtriplett RE .await!() deveríamos conversar separadamente-- Eu inicialmente fui a favor disso também, mas não acho que devemos acertar isso se também não pudermos obter macros postfix em geral, e eu acho há uma grande oposição em relação a eles (com razão muito boa, embora infeliz), e eu realmente gostaria que isso não impedisse a estabilização de await .

Por que não ambos?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Eu vejo o apelo do postfix mais agora, e quase gostando mais dele em alguns cenários. Especialmente com o cheat acima, que é tão simples que nem precisa ser fornecido pelo próprio Rust.

Então, +1 para postfix.

Eu acho que também devemos ter uma função de prefixo, além da versão pós-fixada.

Quanto aos detalhes da sintaxe pós-fixada, não estou tentando dizer que .await!() é a única sintaxe pós-fixada viável; Simplesmente não sou fã do postfix await com um espaço inicial.

Parece aceitável (embora ainda incomum) quando você o formata com uma instrução por linha, mas muito menos razoável quando você formata instruções simples em uma linha.

Para aqueles que não gostam de operadores de palavra-chave Postfix, podemos definir um operador simbólico adequado para await .

No momento, estávamos meio que sem operadores em caracteres ASCII simples para o operador postfix. No entanto, que tal

let x = do_something()⌛.do_somthing_else()⌛;

Se realmente precisamos de ASCII simples, eu vim com (inspirado na forma acima)

let x = do_something()><.do_somthing_else()><;

ou (forma semelhante na posição horizontal)

let x = do_something()>=<.do_somthing_else()>=<;

Outra idéia é fazer da await struct um colchete.

let x = >do_something()<.>do_something_else()<;

Todas essas soluções ASCII compartilham o mesmo problema de passagem que <..> já é excessivamente usado e temos problemas de análise com < e > . No entanto, >< ou >=< pode ser melhor para isso, pois eles não requerem espaço dentro do operador e não abrem < s na posição atual.


Para aqueles que simplesmente não gostam do espaço entre eles, mas OK para operadores de palavras-chave postfix, que tal usar hifens:

let x = do_something()-await.do_something_else()-await;

Sobre ter muitas maneiras diferentes de escrever o mesmo código, eu pessoalmente não gosto disso. A principal razão pela qual é muito mais difícil para as pessoas que são novas ter uma compreensão adequada do que é a maneira correta ou o ponto de obtê-la. A segunda razão é que teremos muitos projetos diferentes que usam sintaxe diferente e seria mais difícil pular entre eles e lê-los (especialmente para os recém-chegados à ferrugem). Eu acho que uma sintaxe diferente deve ser implementada apenas se houver realmente diferença e isso der algumas vantagens. Muito açúcar de código apenas torna muito mais difícil aprender e trabalhar com a linguagem.

@goffrie Sim, concordo que não devemos ter muitas maneiras diferentes de fazer a mesma coisa. No entanto, eu estava apenas propondo alternativas diferentes, a comunidade só precisa escolher uma. Portanto, isso não é realmente uma preocupação.

Além disso, em termos de macro await! não há como impedir o usuário de inventar suas próprias macros para fazer isso de maneira diferente, e Rust pretende habilitar isso. Portanto, "ter muitas maneiras diferentes de fazer a mesma coisa" é inevitável.

Acho que aquela macro burra e simples que mostrei demonstra que não importa o que façamos, os usuários farão o que quiserem de qualquer maneira. Uma palavra-chave, seja ela prefixo ou pós-fixada, pode ser transformada em uma macro de prefixo semelhante a uma função ou presumivelmente em uma macro semelhante a um método pós-fixo, sempre que houver. Mesmo se escolhermos macros semelhantes a funções ou métodos para await , elas podem ser invertidas com outra macro. Realmente não importa.

Portanto, devemos nos concentrar na flexibilidade e formatação. Fornece uma solução que preenche mais facilmente todas essas possibilidades.

Além disso, embora neste curto espaço de tempo eu tenha crescido apegado à sintaxe da palavra-chave postfix, await deve espelhar tudo o que é decidido para yield com geradores, que provavelmente é uma palavra-chave de prefixo. Para usuários que desejam uma solução pós-fixada, provavelmente existirão macros semelhantes a métodos.

Minha conclusão é que uma palavra-chave de prefixo await é a melhor sintaxe padrão por agora, talvez com uma caixa regular fornecendo aos usuários uma macro do tipo função await! e, no futuro, um método do tipo postfix Macro .await!() .

@novacrazy

Além disso, embora neste curto espaço de tempo eu tenha crescido apegado à sintaxe da palavra-chave do postfix, await deve espelhar tudo o que é decidido para yield com geradores, que provavelmente é uma palavra-chave do prefixo.

A expressão yield 42 está no tipo ! , enquanto foo.await está no tipo T onde foo: impl Future<Output = T> . @stjepang faz a analogia certa com ? e return aqui. await não é como yield .

Por que não ambos?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Você precisará nomear a macro de outra forma porque await deve permanecer uma palavra-chave verdadeira.


Por uma variedade de razões, sou contra o prefixo await e ainda mais a forma de bloco await { ... } .

Primeiro, há os problemas de precedência com await expr? onde a precedência consistente é await (expr?) mas você quer (await expr)? . Como solução para os problemas de precedência, alguns sugeriram await? expr além de await expr . Isso envolve await? como uma unidade e um invólucro especial; isso parece injustificado, um desperdício de nosso orçamento complexo e uma indicação de que await expr tem problemas sérios.

Mais importante, o código Rust e, em particular, a biblioteca padrão é fortemente centrado em torno do poder da sintaxe de chamada de ponto e método. Quando await é o prefixo, ele encoraja o usuário a inventar ligações let temporárias ao invés de simplesmente métodos de encadeamento. Esta é a razão pela qual ? é pós-fixada e, pela mesma razão, await também deve ser pós-fixada.

Ainda pior seria await { ... } . Esta sintaxe, se formatada de forma consistente de acordo com rustfmt , se transforma em:

    let x = await { // by analogy with `loop`
        foo.bar.baz.other_thing()
    };

Isso não seria ergonômico e aumentaria significativamente o comprimento vertical das funções.


Em vez disso, acho que esperar, como ? , deve ser pós-fixado, pois isso se encaixa no ecossistema Rust que está centrado no encadeamento de métodos. Várias sintaxes pós-fixadas foram mencionadas; Vou passar por alguns deles:

  1. foo.await!() - Esta é a solução de macro postfix . Embora eu seja fortemente a favor de macros postfix, concordo com @cramertj em https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040 que não devemos fazer isso a menos que também nos comprometamos com postfix macros em geral. Eu também acho que usar uma macro pós-fixada dessa forma dá uma sensação um pouco não de primeira classe; devemos imo evitar fazer uma construção de linguagem usar sintaxe macro.

  2. foo await - Isso não é tão ruim, ele realmente funciona como um operador postfix ( expr op ), mas sinto que há algo faltando nesta formatação (isto é, parece "vazio"); Em contraste, expr? anexa ? diretamente em expr ; não há espaço aqui. Isso faz com que ? pareça visualmente atraente.

  3. foo.await - Foi criticado por se parecer com um acesso de campo; e isso é verdade. Devemos, entretanto, lembrar que await é uma palavra-chave e, portanto, sua sintaxe será destacada como tal. Se você ler o código Rust em seu IDE ou de forma equivalente no GitHub, await estará em uma cor ou negrito diferente de foo . Usando uma palavra-chave diferente, podemos demonstrar isso:

    let x = foo.match?;
    

    Normalmente, os campos também são substantivos, enquanto await é um verbo.

    Embora haja um fator inicial ridículo sobre foo.await , acho que deve ser considerado seriamente como uma sintaxe visualmente atraente e ao mesmo tempo legível.

    Como um bônus, usar .await dá a você o poder do ponto e o preenchimento automático que o ponto geralmente tem em IDEs (consulte a página 56). Por exemplo, você pode escrever foo. e se foo for um futuro, await será mostrado como a primeira escolha. Isso facilita a ergonomia e a produtividade do desenvolvedor, já que alcançar o ponto é algo que muitos desenvolvedores treinaram na memória muscular.

    Em todas as sintaxes pós-fixadas possíveis, apesar das críticas sobre a aparência de acesso a campo, esta continua sendo minha sintaxe favorita.

  4. foo# - Isso usa o sigilo # para aguardar em foo . Eu acho que considerar um sigilo é uma boa ideia, dado que ? também é um sigilo e porque torna a espera mais leve. Combinado com ? , pareceria foo#? - parece OK. No entanto, # não tem uma justificativa específica. Em vez disso, é apenas um sigilo que ainda está disponível.

  5. foo@ - Outro sigilo é @ . Quando combinado com ? , obtemos foo@? . Uma justificativa para este sigilo específico é que ele parece a -ish ( @wait ).

  6. foo! - Finalmente, há ! . Quando combinado com ? , obtemos foo!? . Infelizmente, isso tem um certo sentimento WTF. No entanto, ! parece forçar o valor, que se encaixa "em espera". Há uma desvantagem em que foo!() já é uma invocação legal de macro, portanto, esperar e chamar uma função precisaria ser escrito (foo)!() . Usar foo! como sintaxe também nos roubaria a chance de ter macros de palavras-chave (por exemplo, foo! expr ).

Outro sigilo simples é foo~ . A onda pode ser entendida como "eco" ou "demora". No entanto, não é usado em nenhum lugar da linguagem Rust.

Tilde ~ era usado antigamente para o tipo de heap alocado: https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

? ser reutilizado? Ou isso é muita magia? Qual seria a aparência de impl Try for T: Future ?

@parasyte Sim, me lembro. Mas ainda estava muito longe.

@jethrogb não há como eu ver impl Try trabalhando diretamente, ? explicitamente return s o resultado de Try da função atual enquanto await precisa de yield .

Talvez ? pudesse ter um caso especial para fazer outra coisa no contexto de um gerador de forma que pudesse yield ou return dependendo do tipo de expressão a que se aplica , mas não tenho certeza de como isso seria compreensível. Além disso, como isso interagiria com Future<Output=Result<...>> , você teria que let foo = bar()??; para fazer "aguardar" e, em seguida, obter a variante Ok de Result ( ou ? nos geradores seria baseado em uma característica tristate que pode yield , return ou resolver para um valor com uma única aplicação)?

Essa última observação entre parênteses realmente me faz pensar que poderia ser viável, clique para ver um esboço rápido
enum GenOp<T, U, E> { Break(T), Yield(U), Error(E) }

trait TryGen {
    type Ok;
    type Yield;
    type Error;

    fn into_result(self) -> GenOp<Self::Ok, Self::Yield, Self::Error>;
}
com `foo?` dentro de um gerador expandindo para algo como (embora isso tenha um problema de propriedade e precise também fixar o resultado de `foo`)
loop {
    match TryGen::into_result(foo) {
        GenOp::Break(val) => break val,
        GenOp::Yield(val) => yield val,
        GenOp::Return(val) => return Try::from_error(val.into()),
    }
}

Infelizmente, não vejo como lidar com a variável de contexto waker em um esquema como este, talvez se ? fossem especiais para async vez de geradores, mas se for especial -caso aqui seria bom se fosse utilizável para outros casos de uso de geradores.

Eu tive o mesmo pensamento sobre a reutilização de ? como @jethrogb.

@ Nemo157

não há como ver impl Try trabalhando diretamente, ? explicitamente return s o resultado de Try da função atual enquanto espera precisa de yield .

Talvez eu esteja faltando alguns detalhes sobre ? e o traço Try , mas onde / por que isso é explícito? E um return em um encerramento assíncrono não é essencialmente o mesmo que yield , apenas uma transição de estado diferente?

Talvez ? pudesse ter um caso especial para fazer outra coisa no contexto de um gerador para que pudesse yield ou return dependendo do tipo de expressão a que se aplica , mas não tenho certeza de como isso seria compreensível.

Não vejo por que isso deveria ser confuso. Se você pensa em ? como "continuar ou divergir", então parece natural, IMHO. Concedido, alterar o traço Try para usar nomes diferentes para os tipos de retorno associados ajudaria.

Além disso, como isso interagiria com Future<Output=Result<...>> , você teria que let foo = bar()?? ;

Se você quiser aguardar o resultado e também sair mais cedo de um resultado de Erro, então essa seria a expressão lógica, sim. Eu não acho que um tri-state especial TryGen seria necessário.

Infelizmente não vejo como lidar com a variável de contexto waker em um esquema como este, talvez se? fossem especiais para assíncronos em vez de geradores, mas se for para casos especiais aqui, seria bom se pudessem ser usados ​​para outros casos de uso de geradores.

Eu não entendo essa parte. Você poderia elaborar?

@jethrogb @rolandsteiner Uma estrutura pode implementar Try e Future . Nesse caso, qual deve ? desembrulhar?

@jethrogb @rolandsteiner Uma estrutura pode implementar Try e Future. Nesse caso, qual deveria? desembrulhar?

Não, não poderia, por causa do cobertor impl Tente for T: Futuro.

Por que ninguém está falando sobre a construção explícita e a proposta

mas é tudo sombreamento de bicicleta, acho que devemos nos contentar com a sintaxe de macro simples await!(my_future) pelo menos por agora

mas é tudo sombreamento de bicicleta, acho que devemos nos contentar com a sintaxe de macro simples await!(my_future) pelo menos por agora

Não, não é "apenas" abandonar as bicicletas, como se isso fosse algo banal e insignificante. O fato de await ser um prefixo ou postfix escrito tem um impacto fundamental sobre como o código assíncrono é escrito wrt. encadeamento de métodos e como ele se sente combinável. Estabilizar em await!(future) também implica que await como uma palavra-chave é abandonada, o que torna o uso futuro de await uma palavra-chave impossível. "Pelo menos por agora" sugere que podemos encontrar uma sintaxe melhor posteriormente e desconsidera o débito técnico que isso acarreta. Oponho-me a introduzir conscientemente dívida para uma sintaxe que deve ser substituída posteriormente.

Stabilizing on await! (Future) também implica que await uma palavra-chave seja abandonada, o que torna o uso futuro de await como uma palavra-chave impossível.

poderíamos torná-la uma palavra-chave na próxima época, exigindo a sintaxe de identificação bruta para a macro, assim como fizemos com try .

@rolandsteiner

E um return em um encerramento assíncrono não é essencialmente o mesmo que yield , apenas uma transição de estado diferente?

yield não existe em um fechamento assíncrono, é uma operação introduzida durante o abaixamento de async / await sintaxe para geradores / yield . Na sintaxe do gerador atual yield é bastante diferente de return , se a expansão de ? é feita antes da transformação do gerador, então eu não sei como ele saberia quando inserir a return ou a yield .

Se você quiser aguardar o resultado e também sair mais cedo em um resultado de Erro, então essa seria a expressão lógica, sim.

Pode ser lógico, mas parece uma desvantagem para mim que muitos (a maioria?) Casos em que você está escrevendo funções assíncronas serão preenchidos com ?? duplos para lidar com os erros de E / S.

Infelizmente, não vejo como lidar com a variável de contexto waker ...

Eu não entendo essa parte. Você poderia elaborar?

A transformação assíncrona recebe uma variável waker na função Future::poll gerada, então ela precisa ser passada para a operação await transformada. Atualmente, isso é tratado com uma variável TLS fornecida por std que ambas as transformações referenciam, se ? fosse tratado como um ponto de re-rendimento _ no nível dos geradores_, então a transformação assíncrona perde o caminho para inserir esta referência de variável.

Eu escrevi uma postagem no blog sobre a sintaxe await descrevendo minha preferência há dois meses. No entanto, ele basicamente assumiu uma sintaxe de prefixo e apenas considerou o problema de precedência dessa perspectiva. Aqui estão algumas idéias adicionais agora:

  • Minha opinião geral é que Rust realmente já esticou seu orçamento de estranheza. Seria ideal que a sintaxe assíncrona / aguardar de nível de superfície fosse o mais familiar possível para alguém que vem de JavaScript ou Python ou C #. Seria ideal, a partir dessa perspectiva, divergir apenas em pequenas coisas da norma. As sintaxes pós-fixadas variam em quão longe de divergência elas estão (por exemplo, foo await é menos divergente do que algum sigilo como foo@ ), mas são todas mais divergentes do que o prefixo espera.
  • Também prefiro estabilizar uma sintaxe que não use ! . Todos os usuários que lidam com async / await se perguntam por que await é uma macro em vez de uma construção de fluxo de controle normal, e eu acredito que a história aqui será essencialmente "bem, não poderíamos descobrir uma boa sintaxe, então decidimos fazendo com que pareça uma macro. " Esta não é uma resposta convincente. Eu não acho que a associação entre ! e o fluxo de controle seja realmente suficiente para justificar esta sintaxe: Eu acredito que ! tem uma despesa macro muito específica, o que não é.
  • Estou meio que duvidoso dos benefícios do Postfix Aguardam em geral (não inteiramente, apenas mais ou menos ). Acho que o saldo é um pouco diferente de ? , porque esperar é uma operação mais cara (você cede em um loop até que esteja pronto, em vez de apenas ramificar e retornar uma vez). Eu meio que desconfio de código que esperaria duas ou três vezes em uma única expressão; parece bom para mim dizer que eles devem ser puxados para fora em suas próprias ligações let. Portanto, a troca de try! vs ? não me atrai tão fortemente aqui. Mas também, eu estaria aberto a exemplos de código que as pessoas acham que realmente não deveriam ser incluídos e são mais claros como cadeias de métodos.

Dito isso, foo await é a sintaxe Postfix mais viável que já vi até agora:

  • É relativamente familiar para sintaxe postfix. Tudo o que você precisa aprender é que await vai depois da expressão em vez de antes dela no Rust, em vez de uma sintaxe significativamente diferente.
  • Isso resolve claramente o problema de precedência que envolve tudo isso.
  • O fato de não funcionar bem com encadeamento de método parece quase uma vantagem para mim, ao invés de uma desvantagem, pelas razões que aludi anteriormente. Eu ficaria mais compelido se tivéssemos algumas regras gramaticais que impedissem foo await.method() só porque eu realmente sinto que o método está sendo (sem sentido) aplicado a await , não a foo (embora seja interessante Não sinto isso com foo await? ).

Ainda estou inclinado para uma sintaxe de prefixo, mas acho que await é a primeira sintaxe pós-fixada que parece ter uma chance real para mim.

Nota secundária: sempre é possível usar parênteses para tornar a precedência mais clara:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Isso não é exatamente o ideal, mas considerando que está tentando enfiar muito em uma única linha, acho que é razoável.

E como @earthengine mencionou antes, a versão multilinhas é muito razoável (sem parênteses extras):

let x = x.do_something() await
         .do_another_thing() await;

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Seria ideal que a sintaxe assíncrona / aguardar de nível de superfície fosse o mais familiar possível para alguém que vem de JavaScript ou Python ou C #.

No caso de try { .. } , levamos em consideração a familiaridade com outros idiomas. No entanto, também foi o projeto certo a partir de um ponto de vista de consistência interna com o Rust. Portanto, com todo o devido respeito a essas outras linguagens, a consistência interna no Rust parece mais importante e não acho que a sintaxe do prefixo se encaixa no Rust em termos de precedência ou em como as APIs são estruturadas.

  • Também prefiro estabilizar uma sintaxe que não use ! . Todos os usuários que lidam com async / await se perguntam por que await é uma macro em vez de uma construção de fluxo de controle normal, e eu acredito que a história aqui será essencialmente "bem, não poderíamos descobrir uma boa sintaxe, então decidimos fazendo com que pareça uma macro. " Esta não é uma resposta convincente.

Eu concordo com esse sentimento, .await!() não parecerá de primeira classe o suficiente.

  • Estou meio que duvidoso do benefício do Postfix Aguardar em geral (não inteiramente, apenas _uma espécie de_). Acho que o saldo é um pouco diferente de ? , porque aguardar é uma operação mais cara (você cede em um loop até que esteja pronto, em vez de apenas ramificar e retornar uma vez).

Não vejo o que o custo tem a ver com extrair coisas em ligações let . As cadeias de métodos podem ser e às vezes também são caras. O benefício das ligações let é a) dar a partes suficientemente grandes um nome onde faça sentido para melhorar a legibilidade, b) ser capaz de se referir ao mesmo valor calculado mais de uma vez (por exemplo, por &x ou quando um tipo é copiável).

Eu meio que desconfio de código que esperaria duas ou três vezes em uma única expressão; parece bom para mim dizer que eles devem ser puxados para fora em suas próprias ligações let.

Se você acha que eles devem ser puxados para suas próprias ligações let você ainda pode fazer essa escolha com o postfix await :

let temporary = some_computation() await?;

Para aqueles que discordam e preferem o encadeamento de métodos, o postfix await oferece a possibilidade de escolha. Eu também acho que o postfix segue melhor a leitura da esquerda para a direita e a ordem do fluxo de dados aqui, então mesmo que você extraia para let bindings, eu ainda prefiro o postfix.

Eu também não acho que você precisa esperar duas ou três vezes para que o postfix await seja útil. Considere, por exemplo (este é o resultado de rustfmt ):

    let foo = alpha()
        .beta
        .some_other_stuff()
        .await?
        .even_more_stuff()
        .stuff_and_stuff();

Mas também, eu estaria aberto a exemplos de código que as pessoas acham que realmente não deveriam ser incluídos e são mais claros como cadeias de métodos.

A maior parte do código fúcsia que li parecia não natural quando extraído nas ligações let e com let binding = await!(...)?; .

  • É relativamente familiar para sintaxe postfix. Tudo o que você precisa aprender é que await vai depois da expressão em vez de antes dela no Rust, em vez de uma sintaxe significativamente diferente.

Minha preferência por foo.await aqui é principalmente porque você obtém um bom preenchimento automático e o poder do ponto. Não parece tão radicalmente diferente também. Escrever foo.await.method() também deixa mais claro que .method() é aplicado a foo.await . Então, isso resolve essa preocupação.

  • Isso resolve claramente o problema de precedência que envolve tudo isso.

Não, não se trata apenas de precedência. As cadeias de métodos são igualmente importantes.

  • O fato de não funcionar bem com encadeamento de método parece quase uma vantagem para mim, ao invés de uma desvantagem, pelas razões que aludi anteriormente.

Não sei por que não funciona bem com encadeamento de método.

Eu ficaria mais compelido se tivéssemos algumas regras gramaticais que impedissem foo await.method() só porque realmente sinto que o método está sendo (sem sentido) aplicado a await , não foo (embora, curiosamente, Não sinto isso com foo await? ).

Ao passo que eu não teria como usar foo await se introduzíssemos um recorte de papel de design intencional e evitássemos o encadeamento de métodos com a sintaxe postfix await .

Garantir que cada opção tem um lado negativo, e que uma delas deve, no entanto, acabar sendo escolhida ... uma coisa que me incomoda sobre foo.await é que, mesmo que assumamos que não será literalmente confundido com um campo de estrutura, ainda parece estar acessando um campo de estrutura. A conotação de acesso de campo é que nada particularmente impactante está acontecendo - é uma das operações menos eficazes em Rust. Enquanto isso, a espera é altamente impactante, uma das operações de maior efeito colateral (ela executa as operações de E / S construídas no futuro e tem efeitos de fluxo de controle). Então, quando eu leio foo.await.method() , meu cérebro está me dizendo para pular .await porque é relativamente desinteressante, e eu tenho que usar atenção e esforço para ignorar manualmente esse instinto.

ainda _parece_ acessar um campo de estrutura.

@glaebhoerl Você faz bons pontos; no entanto, o realce de sintaxe não tem impacto / impacto insuficiente em sua aparência e na maneira como seu cérebro processa as coisas? Pelo menos para mim, cor e ousadia são muito importantes ao ler o código, então eu não pularia .await que tem uma cor diferente do resto das coisas.

A conotação de acesso de campo é que nada particularmente impactante está acontecendo - é uma das operações menos eficazes em Rust. Enquanto isso, a espera é altamente impactante, uma das operações de maior efeito colateral (ela executa as operações de E / S construídas no futuro e tem efeitos de fluxo de controle).

Eu concordo totalmente com isso. await é uma operação de fluxo de controle como break ou return e deve ser explícita. A notação postfix proposta parece não natural, como if do Python: compare if c { e1 } else { e2 } com e1 if c else e2 . Ver o operador no final o faz pensar duas vezes, independentemente de qualquer destaque de sintaxe.

Também não vejo como e.await é mais consistente com a sintaxe do Rust do que await!(e) ou await e . Não há outra palavra-chave pós-fixada e, como uma das ideias era colocá-la em maiúsculas e minúsculas no analisador, não acho que isso seja uma prova de consistência.

Há também a questão da familiaridade mencionada @withoutboats . Podemos escolher uma sintaxe estranha e maravilhosa se ela tiver alguns benefícios maravilhosos. No entanto, um postfix await tem?

o realce de sintaxe não tem impacto / impacto insuficiente em sua aparência e na maneira como seu cérebro processa as coisas?

(Boa pergunta, tenho certeza de que teria algum impacto, mas é difícil adivinhar quanto sem realmente tentar (e substituir uma palavra-chave diferente só vai até agora). Já que estamos no assunto ... muito tempo atrás, mencionei que acho que o destaque de sintaxe deve destacar todos os operadores com efeitos de fluxo de controle ( return , break , continue , ? ... e agora await ) em alguma cor especial extra distinta, mas não sou responsável pelo realce de sintaxe de nada e não sei se alguém realmente faz isso.)

Eu concordo totalmente com isso. await é uma operação de fluxo de controle como break ou return e deve ser explícita.

Nós concordamos. As notações foo.await , foo await , foo# , ... são explícitas . Não há espera implícita sendo feita.

Também não vejo como e.await é mais consistente com a sintaxe do Rust do que await!(e) ou await e .

A sintaxe e.await per se não é consistente com a sintaxe do Rust, mas o postfix geralmente se encaixa melhor com ? e como as APIs do Rust são estruturadas (métodos são preferidos em vez de funções livres).

A sintaxe await e? , se associada como (await e)? é completamente inconsistente com a associação de break e return . await!(e) também é inconsistente, pois não temos macros para controle de fluxo e também tem o mesmo problema de outros métodos de prefixo.

Não há outra palavra-chave pós-fixada e, como uma das ideias era colocá-la em maiúsculas e minúsculas no analisador, não acho que isso seja uma prova de consistência.

Não acho que você realmente precise alterar a libsyntax para .await pois já deve ser tratada como uma operação de campo. A lógica prefere ser tratada na resolução ou HIR, onde você a traduz para uma construção especial.

Podemos escolher uma sintaxe estranha e maravilhosa se ela tiver alguns benefícios maravilhosos. No entanto, um postfix await tem?

Como mencionado anteriormente, eu argumento que sim devido ao encadeamento de métodos e à preferência de Rust por chamadas de método.

Não acho que você realmente precise alterar libsyntax para .await, uma vez que já deve ser tratado como uma operação de campo.

Isto é divertido.
Portanto, a ideia é reutilizar a abordagem self / super /...'s, mas para campos em vez de para segmentos de caminho.

Isso efetivamente torna await uma palavra-chave de segmento de caminho (uma vez que passa pela resolução), então você pode proibir identificadores brutos para ela.

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

Não há espera implícita sendo feita.

A ideia surgiu algumas vezes neste tópico (a proposta "esperar implícito").

não temos macros para controle de fluxo

Existe try! (que serviu seu propósito muito bem) e indiscutivelmente o obsoleto select! . Observe que await é "mais forte" do que return , portanto, é razoável esperar que seja mais visível no código do que ? return .

Eu argumento que sim devido ao encadeamento de método e à preferência de Rust por chamadas de método.

Ele também tem uma preferência (mais perceptível) por operadores de fluxo de controle de prefixo.

A espera e? sintaxe, se associada como (await e)? é completamente inconsistente com a forma como interromper e retornar o associado.

Eu prefiro await!(e)? , await { e }? ou talvez até { await e }? - não acho que tenha visto este último e não tenho certeza se funciona.


Eu admito que pode ter um viés da esquerda para a direita. _Observação_

Minha opinião sobre isso parece mudar cada vez que olho para o problema, como se estivesse bancando o advogado do Diabo para mim mesmo. Parte disso é porque estou acostumada a escrever meus próprios futuros e máquinas de estado. Um futuro personalizado com poll é totalmente normal.

Talvez isso deva ser pensado de outra maneira.

Para mim, abstrações de custo zero no Rust se referem a duas coisas: custo zero em tempo de execução e, mais importante, custo zero mentalmente.

Posso raciocinar facilmente sobre a maioria das abstrações em Rust, incluindo futuros, porque eles são apenas máquinas de estado.

Para este fim, deve existir uma solução simples que introduza o mínimo de mágica para o usuário. Os sigilos, especialmente, são uma má ideia, pois parecem desnecessariamente mágicos. Isso inclui .await campos mágicos.

Talvez a melhor solução seja a mais fácil, a macro await! .

Portanto, com todo o devido respeito a essas outras linguagens, a consistência interna no Rust parece mais importante e não acho que a sintaxe do prefixo se encaixa no Rust em termos de precedência ou em como as APIs são estruturadas.

Não vejo como ...? await(foo)? / await { foo }? parece totalmente bem em termos de precedência de operador e como APIs são estruturadas em Rust - sua desvantagem é a prolixidade dos parênteses e (dependendo da sua perspectiva) encadeamento, não quebrando precedentes ou sendo confuso.

Existe try! (que serviu seu propósito muito bem) e indiscutivelmente o obsoleto select! .

Acho que a palavra-chave aqui está obsoleta . Usar try!(...) é um erro difícil no Rust 2018. É um erro difícil agora porque introduzimos uma sintaxe melhor, de primeira classe e pós-fixada.

Observe que await é "mais forte" do que return , então não é irracional esperar que seja mais visível no código do que ? return .

O operador ? também pode ser um efeito colateral (por meio de outras implementações além de Result ) e executa o fluxo de controle, portanto, também é bastante "forte". Quando foi discutido, ? foi acusado de "esconder uma devolução" e ser fácil de ignorar. Acho que essa previsão falhou completamente em se concretizar. A situação re. await parece bastante semelhante a mim.

Ele também tem uma preferência (mais perceptível) por operadores de fluxo de controle de prefixo.

Esses operadores de fluxo de controle de prefixo são digitados em ! type. Enquanto isso, o outro operador de fluxo de controle ? que pega um contexto impl Try<Ok = T, ...> e dá a você um T é pós-fixado.

Não vejo como ...? await(foo)? / await { foo }? parece totalmente bom em termos de precedência de operador e como as APIs são estruturadas em Rust-

A sintaxe await(foo) não é a mesma que await foo se parênteses for necessário para o primeiro e não para o último. O primeiro é sem precedentes, o último tem problemas de precedência escritos. ? como discutimos aqui, na postagem do blog do boat e no Discord. A sintaxe await { foo } é problemática por outros motivos (consulte https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

sua desvantagem é a prolixidade dos parênteses e (dependendo da sua perspectiva) o encadeamento, sem quebrar precedentes ou ser confuso.

Isso é o que quero dizer com "APIs são estruturadas". Acho que os métodos e o encadeamento de métodos são comuns e idiomáticos no Rust. As sintaxes de prefixo e bloco combinam mal com aqueles e com ? .

Posso ser minoria com esta opinião e, se assim for, ignore-me:

Seria justo mover a discussão prefix-vs-postfix para um thread Internals e, em seguida, simplesmente voltar aqui com o resultado? Dessa forma, podemos manter o problema de rastreamento para

@seanmonstar Sim, eu simpatizo fortemente com o desejo de limitar a discussão sobre o rastreamento de problemas e ter problemas que são, na verdade, apenas atualizações de status. Este é um dos problemas que espero que possamos resolver com algumas revisões de como gerenciamos o processo RFC e os problemas em geral. Por enquanto, abri um novo problema aqui para usarmos em discussão.

IMPORTANTE PARA TODOS: mais await discussão sobre a sintaxe deve ir aqui .

Bloqueando temporariamente por um dia para garantir que uma discussão futura sobre a sintaxe await ocorra sobre o problema apropriado.

Na terça-feira, 15 de janeiro de 2019 às 07:10:32 AM-0800, Pauan escreveu:

Nota secundária: sempre é possível usar parênteses para tornar a precedência mais clara:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Isso derrota o principal benefício do postfix await: "apenas mantenha
escrevendo / lendo ". Postfix await, como postfix ? , permite o fluxo de controle
para continuar movendo da esquerda para a direita:

foo().await!()?.bar().await!()

Se await! fosse prefixo, ou de volta quando try! era prefixo, ou se você tiver
entre parênteses, então você tem que voltar para o lado esquerdo do
a expressão ao escrever ou ler.

EDITAR: Eu estava lendo comentários do início ao fim via e-mail e não vi os comentários "mover conversa para outro problema" até depois de enviar este e-mail.

Relatório de status de espera assíncrona:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Eu queria postar uma atualização rápida sobre o status do async-await
esforço. A versão curta é que estamos na reta final para
algum tipo de estabilização, mas permanecem alguns
questões a serem superadas.

Anunciando o grupo de trabalho de implementação

Como parte desse esforço, tenho o prazer de anunciar que formamos um
grupo de trabalho de implementação async-await . Este grupo de trabalho
faz parte de todo o esforço de espera assíncrona, mas com foco no
implementação e faz parte da equipe de compiladores. Se você gostaria de
ajude a obter async-aguarde sobre a linha de chegada, nós temos uma lista de problemas
onde definitivamente gostaríamos de ajuda (continue lendo).

Caso tenha interesse em participar, temos um "horário comercial"programado para terça-feira (veja o [calendário da equipe do compilador]) - se você
pode aparecer então no [Zulip], seria o ideal! (Mas se não, basta inserir qualquer
Tempo.)

...

Quando std::future::Future estará estável? Tem que esperar por assíncrono esperar? Acho que é um design muito bom e gostaria de começar a portar código para ele. (Existe um calço para usá-lo no estábulo?)

@ry, veja o novo problema de rastreamento: https://github.com/rust-lang/rust/issues/59113

Outro problema do compilador para async / await: https://github.com/rust-lang/rust/issues/59245

Observe também que https://github.com/rust-lang-nursery/futures-rs/issues/1199 na postagem superior pode ser desmarcado, pois agora está corrigido.

Parece que há um problema com HRLB e fechamentos assíncronos: https://github.com/rust-lang/rust/issues/59337. (Embora, ao refazer a leitura do RFC, isso não especifique realmente que os fechamentos assíncronos estão sujeitos à mesma captura de tempo de vida do argumento que a função assíncrona tem).

Sim, os fechamentos assíncronos têm vários problemas e não devem ser incluídos na rodada inicial de estabilização. O comportamento atual pode ser emulado com um bloco closure + assíncrono, e no futuro eu adoraria ver uma versão que permitisse referenciar upvars de closure do futuro retornado.

Acabei de notar que atualmente await!(fut) requer que fut seja Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

Isso é esperado? Não parece estar no RFC.

@Ekleog que não está dando o erro await! , await! empilhar conceitualmente o futuro passado para permitir que !Unpin futuros sejam usados ​​( exemplo rápido de playground ). O erro vem da restrição em impl Future for Box<impl Future + Unpin> , que requer que o futuro seja Unpin para impedi-lo de fazer algo como:

// where Foo: Future + !Unpin
let mut foo: Box<Foo> = ...;
Pin::new(&mut foo).poll(cx);
let mut foo = Box::new(*foo);
Pin::new(&mut foo).poll(cx);

Como Box é Unpin e permite mover o valor dele, você pode pesquisar o futuro uma vez em um local de heap, mover o futuro fora da caixa e colocá-lo em um novo local de heap e pesquisar isso de novo.

Wait deve possivelmente ter uma caixa especial para permitir Box<dyn Future> uma vez que consome o futuro

Talvez a característica IntoFuture deva ser ressuscitada para await! ? Box<dyn Future> pode implementar isso convertendo para Pin<Box<dyn Future>> .

Aí vem meu próximo bug com async / await: parece que está usando um tipo associado a um parâmetro de tipo no tipo de retorno de uma inferência de async fn quebras: https://github.com/rust-lang/rust/ questões / 60414

Além de potencialmente adicionar # 60414 à lista do post principal (não sei se ainda está sendo usado - talvez seja melhor apontar para o rótulo do github?), Acho que a “Resolução de rust-lang / rfcs # 2418 ”pode ser marcado, já que o traço Future IIRC foi recentemente estabilizado.

Acabei de chegar de um post do Reddit e devo dizer que não gosto de sintaxe postfix. E parece que a maioria do Reddit também não gosta.

Eu prefiro escrever

let x = (await future)?

do que aceitar essa sintaxe estranha.

Quanto ao encadeamento, posso refatorar meu código para evitar ter mais de 1 await .

Além disso, JavaScript no futuro pode fazer isso ( proposta de pipeline inteligente ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Se o prefixo await for implementado, isso não significa que await não pode ser encadeado.

@KSXGitHub este não é realmente o lugar para esta discussão, mas a lógica é delineada aqui e há boas razões para isso que foram pensadas ao longo de muitos meses por muitas pessoas https://boats.gitlab.io/blog/post / aguardar decisão /

@KSXGitHub Embora eu também não https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https: //internals.rust- lang.org/t/a-final-proposal-for-await-syntax/ e em vários outros lugares. Muitas pessoas expressaram sua preferência por lá, e você não está trazendo novos argumentos para o assunto.

Não discuta as decisões de design aqui, há um tópico para esse propósito explícito

Se você planeja comentar aí, por favor, tenha em mente que a discussão já se desenrolou bastante: certifique-se de ter algo substancial a dizer e certifique-se de que não tenha sido dito antes no tópico.

@semoutboats, pelo meu entendimento, a sintaxe final já foi acordada, talvez seja hora de marcá-la como Concluída? :corar:

A intenção é estabilizar a tempo para o próximo corte beta em 4 de julho ou os bugs de bloqueio exigirão outro ciclo para serem resolvidos? Existem muitos problemas em aberto sob a tag A-async-await, embora eu não tenha certeza de quantos deles são críticos.

Aha, desconsidere isso, eu acabei de descobrir o rótulo AsyncAwait-Blocking .

Olá! Quando devemos esperar o lançamento estável desse recurso? E como posso usar isso em compilações noturnas?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 contém informações sobre a data de lançamento prevista e muito mais

Existe um plano para implementar automaticamente Unpin para futuros gerados por async fn ?

Especificamente, estou me perguntando se o Unpin não está disponível automaticamente devido ao próprio código Future gerado, ou se porque podemos usar referências como argumentos

@DoumanAsh Suponho que se um fn assíncrono nunca tiver nenhuma autorreferência ativa nos pontos de rendimento, então o Future gerado poderia implementar Desafixar, talvez?

Eu acho que isso precisaria ser acompanhado por algumas mensagens de erro bastante úteis dizendo "não Unpin por causa de _este_ pedir emprestado" + uma dica de "alternativamente, você pode encaixotar neste futuro"

O PR de estabilização em # 63209 observa que "Todos os bloqueadores agora estão fechados." e foi lançado à noite em 20 de agosto, portanto, indo para o corte beta no final desta semana. Parece interessante notar que, desde 20 de agosto, alguns novos problemas de bloqueio foram registrados (conforme rastreado pela tag AsyncAwait-Blocking). Dois deles (# 63710, # 64130) parecem ser agradáveis ​​de se ter e não impediriam a estabilização, no entanto, há três outros problemas (# 64391, # 64433, # 64477) que valem a pena ser discutidos. Esses três últimos problemas estão relacionados, todos eles surgindo devido ao PR # 64292, que foi lançado para resolver o problema de AsyncAwait-Blocking # 63832. Um PR, # 64584, já pousou na tentativa de resolver a maior parte dos problemas, mas as três questões permanecem em aberto por enquanto.

O forro de prata é que os três bloqueadores abertos sérios parecem se referir ao código que deveria compilar, mas não compila atualmente. Nesse sentido, seria compatível com versões anteriores para consertar posteriormente sem impedir a beta-ização e eventual estabilização de async / await. No entanto, estou me perguntando se alguém da equipe da lang pensa que algo aqui é preocupante o suficiente para sugerir que async / await deve ser ativado todas as noites para outro ciclo (o que, por mais desagradável que possa parecer, é o ponto do cronograma de lançamento rápido depois de tudo).

@bstrie Estamos apenas reutilizando "AsyncAwait-Blocking" por falta de um rótulo melhor para identificá-los como "alta prioridade", eles não estão bloqueando na verdade. Devemos renovar o sistema de rotulagem em breve para torná-lo menos confuso, cc @nikomatsakis.

... Nada bom ... perdemos async-await no esperado 1.38. Ter que esperar 1,39, só porque alguns "problemas" que não contavam ...

@earthengine Não acho que seja uma avaliação justa da situação. Todas as questões que surgiram valeram a pena ser levadas a sério. Não seria bom pousar de forma assíncrona e esperar apenas para que as pessoas se deparassem com esses problemas tentando usá-lo na prática :)

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