Não encontrei muitos recursos sobre isso: fiquei pensando se é possível / uma boa idéia conseguir escrever código assíncrono de maneira síncrona.
Por exemplo, aqui está um código JavaScript que recupera o número de usuários armazenados em um banco de dados (uma operação assíncrona):
getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });
Seria bom poder escrever algo assim:
const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);
E assim o compilador cuidaria automaticamente da espera da resposta e, em seguida, executaria console.log
. Ele sempre aguardará a conclusão das operações assíncronas antes que os resultados tenham que ser usados em qualquer outro lugar. Nós usaríamos muito menos promessas de retorno de chamada, assíncrono / aguardado ou o que quer que seja, e nunca precisaríamos nos preocupar se o resultado de uma operação está disponível imediatamente ou não.
Os erros ainda seriam gerenciáveis ( nbOfUsers
obtiveram um número inteiro ou um erro?) Usando try / catch ou algo parecido com os opcionais, como no idioma Swift .
É possível? Pode ser uma péssima ideia / uma utopia ... não sei.
await
saTask<T>
para convertê-lo emT
async
/ emawait
vez disso, o que torna explícitas as partes assíncronas da execução.Respostas:
Assíncrono / espera é exatamente o gerenciamento automatizado que você propõe, embora com duas palavras-chave extras. Por que eles são importantes? Além da compatibilidade com versões anteriores?
Sem pontos explícitos onde uma corotina pode ser suspensa e retomada, precisaríamos de um sistema de tipos para detectar onde um valor esperado deve ser aguardado. Muitas linguagens de programação não possuem esse tipo de sistema.
Ao tornar a espera um valor explícito, também podemos passar valores aguardáveis como objetos de primeira classe: promessas. Isso pode ser super útil ao escrever código de ordem superior.
O código assíncrono tem efeitos muito profundos no modelo de execução de uma linguagem, semelhante à ausência ou presença de exceções na linguagem. Em particular, uma função assíncrona só pode ser aguardada por funções assíncronas. Isso afeta todas as funções de chamada! Mas e se mudarmos uma função de não assíncrona para assíncrona no final dessa cadeia de dependência? Isso seria uma alteração incompatível com versões anteriores ... a menos que todas as funções sejam assíncronas e todas as chamadas de funções sejam aguardadas por padrão.
E isso é altamente indesejável porque tem implicações muito ruins no desempenho. Você não seria capaz de simplesmente devolver valores baratos. Cada chamada de função se tornaria muito mais cara.
O assíncrono é ótimo, mas algum tipo de assíncrono implícito não funciona na realidade.
Linguagens funcionais puras como Haskell têm um pouco de escape porque a ordem de execução é amplamente não especificada e não é observável. Ou formulado de maneira diferente: qualquer ordem específica de operações deve ser explicitamente codificada. Isso pode ser bastante complicado para programas do mundo real, especialmente aqueles programas pesados de E / S para os quais o código assíncrono é muito bom.
fonte
someValue ifItIsAFuture [self| self messageIWantToSend]
porque é difícil integrar com código genérico.par
praticamente qualquer lugar no código Haskell puro e obter paralelismo gratuitamente.O que está faltando é o objetivo das operações assíncronas: elas permitem que você faça uso do seu tempo de espera!
Se você transformar uma operação assíncrona, como solicitar algum recurso de um servidor, em uma operação síncrona, aguardando implicitamente e imediatamente a resposta, seu encadeamento não poderá fazer mais nada com o tempo de espera . Se o servidor demorar 10 milissegundos para responder, haverá cerca de 30 milhões de ciclos de CPU no desperdício. A latência da resposta se torna o tempo de execução da solicitação.
A única razão pela qual os programadores inventaram operações assíncronas é ocultar a latência de tarefas inerentemente demoradas por trás de outros cálculos úteis . Se você pode preencher o tempo de espera com um trabalho útil, isso economiza o tempo da CPU. Se você não pode, bem, nada é perdido pela operação sendo assíncrona.
Portanto, recomendo adotar as operações assíncronas que seus idiomas fornecem a você. Eles estão lá para economizar seu tempo.
fonte
Alguns fazem.
Eles ainda não são populares (as) porque o assíncrono é um recurso relativamente novo, que só agora tivemos uma boa ideia, mesmo que seja um bom recurso, ou como apresentá-lo aos programadores de uma maneira amigável / utilizável / expressivo / etc. Os recursos assíncronos existentes são amplamente agregados aos idiomas existentes, o que exige uma abordagem de design um pouco diferente.
Dito isto, não é claramente uma boa ideia fazer em qualquer lugar. Uma falha comum é fazer chamadas assíncronas em um loop, serializando efetivamente sua execução. Ter implícitas chamadas assíncronas pode obscurecer esse tipo de erro. Além disso, se você oferecer suporte à coerção implícita de um
Task<T>
(ou equivalente do seu idioma) paraT
, isso poderá adicionar um pouco de complexidade / custo ao seu datilógrafo e relatório de erros quando não estiver claro qual dos dois o programador realmente queria.Mas esses não são problemas intransponíveis. Se você quisesse apoiar esse comportamento, certamente poderia, embora houvesse trade-offs.
fonte
Existem idiomas que fazem isso. Mas, na verdade, não há muita necessidade, pois ela pode ser facilmente realizada com os recursos de idioma existentes.
Desde que você tenha alguma maneira de expressar assincronia, você pode implementar futuros ou promessas apenas como um recurso de biblioteca, não precisará de nenhum recurso especial de idioma. E, desde que você expresse alguns Proxies Transparentes , poderá unir os dois recursos e ter Futuros Transparentes .
Por exemplo, no Smalltalk e seus descendentes, um objeto pode mudar sua identidade, literalmente "se tornar" um objeto diferente (e, de fato, o método que faz isso é chamado
Object>>become:
).Imagine uma computação de longa duração que retorne a
Future<Int>
. IssoFuture<Int>
tem todos os mesmos métodos queInt
, exceto com implementações diferentes.Future<Int>
O+
método de não adicionar outro número e retornar o resultado, retorna um novoFuture<Int>
que encerra a computação. E assim por diante. Métodos que não podem ser implementados de maneira sensata retornando aFuture<Int>
, em vez disso automaticamenteawait
resultarão e depois chamarãoself become: result.
, o que fará com que o objeto atualmente em execução (self
ou seja, oFuture<Int>
) se torne literalmente oresult
objeto, ou seja, a partir de agora a referência ao objeto que costumava ser aFuture<Int>
é agora emInt
todo lugar, completamente transparente para o cliente.Não é necessário nenhum recurso especial de linguagem relacionada à assincronia.
fonte
Future<T>
eT
compartilhar alguma interface comum e eu uso a funcionalidade dessa interface. Devebecome
o resultado e, em seguida, usar a funcionalidade ou não? Estou pensando em coisas como um operador de igualdade ou uma representação de depuração de string.a + b
, ambos os números inteiros, não importa se aeb estão disponíveis imediatamente ou mais tarde, apenas escrevemosa + b
(possibilitandoInt + Future<Int>
)Future<T>
eT
porque, do seu ponto de vista, não existeFuture<T>
, apenas aT
. Agora, é claro que existem muitos desafios de engenharia em torno de como tornar isso eficiente, quais operações devem ser bloqueadas versus não bloqueadoras etc., mas isso é realmente independente de você fazer isso como um recurso de linguagem ou de biblioteca. A transparência foi um requisito estipulado pelo OP na questão, não vou argumentar que é difícil e pode não fazer sentido.Eles fazem (bem, a maioria deles). O recurso que você está procurando é chamado de threads .
Os threads têm seus próprios problemas, no entanto:
Como o código pode ser suspenso a qualquer momento , você nunca pode assumir que as coisas não mudam "sozinhas". Ao programar com threads, você perde muito tempo pensando em como seu programa deve lidar com as coisas mudando.
Imagine que um servidor de jogo esteja processando o ataque de um jogador contra outro jogador. Algo assim:
Três meses depois, um jogador descobre que, ao ser morto e desconectado precisamente quando
attacker.addInventoryItems
está em execução,victim.removeInventoryItems
falhará, ele poderá manter seus itens e o atacante também receberá uma cópia de seus itens. Ele faz isso várias vezes, criando um milhão de toneladas de ouro do nada e quebrando a economia do jogo.Como alternativa, o atacante pode sair enquanto o jogo envia uma mensagem para a vítima, e ele não recebe uma etiqueta de "assassino" acima da cabeça, para que a próxima vítima não fuja dele.
Como o código pode ser suspenso a qualquer momento , você precisa usar bloqueios em qualquer lugar ao manipular estruturas de dados. Dei um exemplo acima que tem conseqüências óbvias em um jogo, mas pode ser mais sutil. Considere adicionar um item ao início de uma lista vinculada:
Isso não é um problema se você disser que os threads só podem ser suspensos quando estão fazendo E / S, e não a qualquer momento. Mas tenho certeza de que você pode imaginar uma situação em que há uma operação de E / S - como o log:
Como o código pode ser suspenso a qualquer momento , é possível que haja muito estado para salvar. O sistema lida com isso, fornecendo a cada thread uma pilha totalmente separada. Mas a pilha é muito grande, então você não pode ter mais do que 2000 threads em um programa de 32 bits. Ou você pode reduzir o tamanho da pilha, correndo o risco de torná-la muito pequena.
fonte
Muitas das respostas aqui são enganosas, porque, embora a pergunta estivesse literalmente perguntando sobre programação assíncrona e não E / S não bloqueadora, não acho que possamos discutir uma sem discutir a outra nesse caso específico.
Embora a programação assíncrona seja inerentemente, bem, assíncrona, a razão de ser da programação assíncrona é principalmente para evitar o bloqueio de threads do kernel. O Node.js usa a assíncrona por meio de retornos de chamada ou
Promise
s para permitir que as operações de bloqueio sejam despachadas a partir de um loop de eventos e o Netty em Java usa a assincronicidade por meio de retornos de chamada ouCompletableFuture
s para fazer algo semelhante.No entanto, o código sem bloqueio não requer assincronicidade . Depende do quanto sua linguagem de programação e tempo de execução estão dispostos a fazer por você.
Go, Erlang e Haskell / GHC podem lidar com isso para você. Você pode escrever algo como
var response = http.get('example.com/test')
e fazer com que ele libere um thread do kernel nos bastidores enquanto aguarda uma resposta. Isso é feito por goroutines, processos Erlang ou liberaçãoforkIO
de threads do kernel nos bastidores ao bloquear, permitindo que ele faça outras coisas enquanto aguarda uma resposta.É verdade que a linguagem não pode realmente lidar com a assincronicidade para você, mas algumas abstrações permitem que você vá além de outras, como continuações não limitadas ou corotinas assimétricas. No entanto, a principal causa do código assíncrono, bloqueando as chamadas do sistema, pode absolutamente ser abstraída do desenvolvedor.
Node.js e Java suportam código não-bloqueador assíncrono , enquanto Go e Erlang suportam código não-bloqueador síncrono . Ambas são abordagens válidas com diferentes vantagens e desvantagens.
Meu argumento bastante subjetivo é que aqueles que argumentam contra tempos de execução que gerenciam não-bloqueio em nome do desenvolvedor são como aqueles que argumentam contra a coleta de lixo nos primeiros anos. Sim, incorre em um custo (neste caso principalmente em mais memória), mas facilita o desenvolvimento e a depuração e torna as bases de código mais robustas.
Eu pessoalmente argumentaria que o código não-bloqueador assíncrono deve ser reservado para a programação de sistemas no futuro e as pilhas de tecnologia mais modernas devem migrar para tempos de execução não-bloqueadores síncronos para o desenvolvimento de aplicativos.
fonte
waitpid(..., WNOHANG)
falhará se tiver que bloquear. Ou "síncrono" aqui significa "não há retornos de chamada / máquinas de estado / loops de eventos visíveis pelo programador"? Mas, para o seu exemplo do Go, ainda preciso aguardar explicitamente o resultado de uma goroutine lendo de um canal, não? Como isso é menos assíncrono que async / wait em JS / C # / Python?Se estou lendo direito, você está pedindo um modelo de programação síncrona, mas uma implementação de alto desempenho. Se isso estiver correto, isso já estará disponível para nós na forma de linhas verdes ou processos de, por exemplo, Erlang ou Haskell. Então, sim, é uma excelente ideia, mas a atualização para os idiomas existentes nem sempre pode ser tão suave quanto você gostaria.
fonte
Agradeço a pergunta e considero que a maioria das respostas é apenas defensiva do status quo. No espectro de idiomas de baixo a alto nível, estamos presos há muito tempo. O próximo nível mais alto será claramente uma linguagem menos focada na sintaxe (a necessidade de palavras-chave explícitas como wait e async) e muito mais sobre a intenção. (Crédito óbvio para Charles Simonyi, mas pensando em 2019 e no futuro.)
Se eu disse a um programador, escreva algum código que simplesmente busque um valor em um banco de dados, você pode assumir com segurança que eu quero dizer "e BTW, não desligue a interface do usuário" e "não introduza outras considerações que mascaram com dificuldade a localização de bugs " Os programadores do futuro, com uma próxima geração de linguagens e ferramentas, certamente serão capazes de escrever código que simplesmente busca um valor em uma linha de código e parte daí.
O idioma de nível mais alto seria falar inglês e confiar na competência do executor de tarefas para saber o que você realmente deseja fazer. (Pense no computador em Star Trek, ou pergunte algo ao Alexa.) Estamos longe disso, mas aproximando-nos mais, e minha expectativa é que a linguagem / compilador possa ser mais para gerar código robusto e intencional sem ir tão longe quanto possível. precisando de IA.
Por um lado, existem novas linguagens visuais, como o Scratch, que fazem isso e não são atoladas com todos os aspectos técnicos sintáticos. Certamente, há muito trabalho nos bastidores para que o programador não precise se preocupar com isso. Dito isso, não estou escrevendo software de classe empresarial no Scratch; portanto, como você, tenho a mesma expectativa de que é hora de linguagens de programação maduras gerenciarem automaticamente o problema síncrono / assíncrono.
fonte
O problema que você está descrevendo é duplo.
Existem algumas maneiras de conseguir isso, mas elas basicamente se resumem a
foo(4, 7, bar, quux)
.Para (1), estou reunindo bifurcação e execução de vários processos, gerando vários threads do kernel e implementações de threads verdes que agendam threads do nível de tempo de execução da linguagem nos threads do kernel. Da perspectiva do problema, eles são os mesmos. Neste mundo, nenhuma função desiste ou perde o controle da perspectiva de seu segmento . O fio em si , por vezes, não tem controle e às vezes não está em execução, mas você não desista controle de seu próprio segmento no mundo. Um sistema adequado a este modelo pode ou não ter a capacidade de gerar novos threads ou ingressar em threads existentes. Um sistema adequado a este modelo pode ou não ter a capacidade de duplicar um thread como o do Unix
fork
.(2) é interessante. Para fazer justiça, precisamos falar sobre formas de introdução e eliminação.
Vou mostrar por que o implícito
await
não pode ser adicionado a uma linguagem como Javascript de uma maneira compatível com versões anteriores. A idéia básica é que, expondo promessas ao usuário e fazendo uma distinção entre contextos síncronos e assíncronos, o Javascript vazou um detalhe de implementação que impede o tratamento uniforme de funções síncronas e assíncronas. Há também o fato de que você não pode fazerawait
uma promessa fora de um corpo de função assíncrona. Essas opções de design são incompatíveis com "tornar a assincronia invisível para o chamador".Você pode introduzir uma função síncrona usando um lambda e eliminá-lo com uma chamada de função.
Introdução da função síncrona:
Eliminação da função síncrona:
Você pode contrastar isso com a introdução e eliminação de funções assíncronas.
Introdução à função assíncrona
Eliminação de função assíncrona (nota: válida apenas dentro de uma
async
função)O problema fundamental aqui é que uma função assíncrona também é uma função síncrona que produz um objeto de promessa .
Aqui está um exemplo de chamada de uma função assíncrona de forma síncrona no repl do node.js.
Hipóteses, você pode ter um idioma, mesmo que digitado dinamicamente, em que a diferença entre as chamadas de função assíncrona e síncrona não seja visível no site da chamada e possivelmente não seja visível no site da definição.
É possível usar uma linguagem como essa e reduzi-la para Javascript, você apenas precisa efetivamente tornar todas as funções assíncronas.
fonte
Com as goroutines do idioma Go e o tempo de execução do idioma Go, você pode escrever todo o código como se fosse sincronizado. Se uma operação bloquear em uma goroutine, a execução continuará em outras goroutines. E com canais você pode se comunicar facilmente entre goroutines. Isso geralmente é mais fácil do que retornos de chamada como em Javascript ou assíncrono / aguardar em outros idiomas. Veja https://tour.golang.org/concurrency/1 para alguns exemplos e uma explicação.
Além disso, não tenho experiência pessoal com isso, mas ouvi dizer que Erlang tem instalações semelhantes.
Portanto, sim, existem linguagens de programação como Go e Erlang, que resolvem o problema síncrono / assíncrono, mas infelizmente ainda não são muito populares. À medida que esses idiomas crescem em popularidade, talvez as instalações que eles fornecem também sejam implementadas em outros idiomas.
fonte
go ...
, por isso parece semelhante aawait ...
não?go
. E praticamente qualquer chamada que possa bloquear é feita de forma assíncrona pelo tempo de execução, que alterna para uma goroutine diferente nesse meio tempo (multitarefa cooperativa). Você aguarda aguardando uma mensagem.await
leitura de um canal<- ch
.Há um aspecto muito importante que ainda não foi levantado: a reentrada. Se você tiver qualquer outro código (ou seja: loop de evento) executado durante a chamada assíncrona (e se você não tiver, por que precisa de assíncrona?), O código poderá afetar o estado do programa. Você não pode ocultar as chamadas assíncronas do chamador porque o chamador pode depender de partes do estado do programa para permanecer inalterado durante a chamada de função. Exemplo:
Se
bar()
for uma função assíncrona, pode ser possívelobj.x
mudar durante a execução. Isso seria inesperado sem nenhuma dica de que a barra é assíncrona e que esse efeito é possível. A única alternativa seria suspeitar que todas as funções / métodos possíveis sejam assíncronos, buscar novamente e verificar novamente qualquer estado não local após cada chamada de função. Isso é propenso a erros sutis e pode nem ser possível, se algum estado não local for buscado por meio de funções. Por isso, o programador precisa estar ciente de quais funções têm o potencial de alterar o estado do programa de maneiras inesperadas:Agora é claramente visível que
bar()
é uma função assíncrona, e a maneira correta de lidar com isso é verificar novamente o valor esperadoobj.x
posteriormente e lidar com quaisquer alterações que possam ter ocorrido.Como já observado por outras respostas, linguagens funcionais puras como Haskell podem escapar completamente desse efeito, evitando a necessidade de qualquer estado compartilhado / global. Como não tenho muita experiência com linguagens funcionais, provavelmente sou contra isso, mas não acho que a falta do estado global seja uma vantagem ao escrever aplicativos maiores.
fonte
No caso do Javascript, que você usou na sua pergunta, há um ponto importante a ser observado: o Javascript é de thread único e a ordem de execução é garantida desde que não haja chamadas assíncronas.
Então, se você tem uma sequência como a sua:
Você tem a garantia de que nada mais será executado nesse meio tempo. Não há necessidade de bloqueios ou algo semelhante.
No entanto, se
getNbOfUsers
for assíncrono, então:significa que, enquanto
getNbOfUsers
é executado, a execução é gerada e outro código pode ser executado no meio. Por sua vez, isso pode exigir algum bloqueio, dependendo do que você está fazendo.Portanto, é uma boa idéia estar ciente quando uma chamada é assíncrona e quando não é, pois em algumas situações você precisará tomar precauções adicionais que não precisaria se a chamada fosse síncrona.
fonte
getNbOfUsers()
retornasse uma promessa. Mas esse é exatamente o ponto da minha pergunta: por que precisamos escrevê-lo explicitamente como assíncrono? O compilador pode detectá-lo e manipulá-lo automaticamente de uma maneira diferente.Está disponível no C ++ como
std::async
desde o C ++ 11.E com C ++ 20, podem ser usadas corotinas:
fonte
await
(ouco_await
, neste caso) em primeiro lugar?