Por que as linguagens de programação não gerenciam automaticamente o problema síncrono / assíncrono?

27

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 ​​( nbOfUsersobtiveram 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.

Cinn
fonte
58
Eu realmente não entendo sua pergunta. Se você "sempre espera a operação assíncrona", não é uma operação assíncrona, é uma operação síncrona. Você pode esclarecer? Talvez dê uma especificação do tipo de comportamento que você está procurando? Além disso, "o que você acha disso" está fora de tópico em Engenharia de Software . Você precisa formular sua pergunta no contexto de um problema concreto, que tenha uma resposta única, inequívoca, canônica e objetivamente correta.
Jörg W Mittag 29/03
4
@ JörgWMittag Eu imagino um C # hipotético que implicitamente awaitsa Task<T>para convertê-lo emT
Caleth
6
O que você propõe não é factível. Não cabe ao compilador decidir se você deseja aguardar o resultado ou talvez atire e esqueça. Ou execute em segundo plano e aguarde mais tarde. Por que se limitar assim?
freakish
5
Sim, é uma péssima ideia. Basta usar async/ em awaitvez disso, o que torna explícitas as partes assíncronas da execução.
Bergi 29/03
5
Quando você diz que duas coisas acontecem simultaneamente, você está dizendo que não há problema em que essas coisas aconteçam em qualquer ordem. Se o seu código não tem como deixar claro quais reordenamentos não quebrarão as expectativas do seu código, ele não poderá torná-lo simultâneo.
Rob

Respostas:

65

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.

amon
fonte
2
Você não precisa necessariamente de um sistema de tipos. Futuros transparentes, por exemplo, ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, podem ser facilmente implementados sem o suporte do sistema ou idioma. No Smalltalk e seus descendentes, um objeto pode alterar sua identidade de forma transparente; no ECMAScript, ele pode alterar sua forma de forma transparente. Isso é tudo o que você precisa para tornar o Futuro transparente, sem necessidade de suporte ao idioma para assincronia.
Jörg W Mittag
6
@ JörgWMittag Eu entendo o que você está dizendo e como isso poderia funcionar, mas futuros transparentes sem um sistema de tipos dificultam a obtenção simultânea de futuros de primeira classe, não? Eu precisaria de uma maneira de selecionar se quero enviar mensagens para o futuro ou o valor do futuro, de preferência algo melhor do que someValue ifItIsAFuture [self| self messageIWantToSend]porque é difícil integrar com código genérico.
amon
8
@amon "Eu posso escrever meu código assíncrono, pois promessas e promessas são mônadas." Mônadas não são realmente necessárias aqui. Thunks são essencialmente apenas promessas. Como quase todos os valores em Haskell estão em caixas, quase todos os valores em Haskell já são promessas. É por isso que você pode lançar parpraticamente qualquer lugar no código Haskell puro e obter paralelismo gratuitamente.
DarthFennec 29/03
2
Async / waitit me lembra a continuação da mônada.
les
3
De fato, as exceções e async / wait são exemplos de efeitos algébricos .
Alex Reinking
21

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.

cmaster
fonte
Eu estava pensando em uma linguagem funcional em que as operações não estão bloqueando, portanto, mesmo que tenha uma sintaxe síncrona, um cálculo de longa duração não bloqueará o thread.
Cinn
6
@ Cinn Não achei isso na pergunta, e o exemplo na pergunta é Javascript, que não possui esse recurso. No entanto, geralmente é bastante difícil para um compilador encontrar oportunidades significativas de paralelização conforme você descreve: A exploração significativa de um recurso desse tipo exigiria que o programador pensasse explicitamente sobre o que colocaria logo após uma longa chamada de latência. Se você tornar o tempo de execução inteligente o suficiente para evitar esse requisito no programador, ele provavelmente consumirá a economia de desempenho, pois precisaria paralelizar agressivamente entre as chamadas de função.
cmaster 29/03
2
Todos os computadores esperam na mesma velocidade.
Bob Jarvis - Restabelecer Monica
2
@BobJarvis Sim. Mas eles diferem em quanto trabalho poderiam ter feito no tempo de espera ...
cmaster 30/03
13

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) para T, 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.

Telastyn
fonte
1
Eu acho uma idéia poderia ser para embrulhar tudo em funções assíncronas, as tarefas síncronas só iria resolver imediatamente e nós temos de tudo, uma espécie de pega (Edit: @amon explicou por que é uma má ideia ...)
CINN
8
Você pode dar alguns exemplos para " Some do ", por favor?
Bergi 30/03
2
A programação assíncrona não é de forma alguma nova, mas hoje em dia as pessoas precisam lidar com isso com mais frequência.
Cúbico
1
@ Cubic - é como um recurso de linguagem, tanto quanto eu sei. Antes, eram apenas (inábil) funções da terra do usuário.
Telastyn 30/03
12

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>. Isso Future<Int>tem todos os mesmos métodos que Int, exceto com implementações diferentes. Future<Int>O +método de não adicionar outro número e retornar o resultado, retorna um novo Future<Int>que encerra a computação. E assim por diante. Métodos que não podem ser implementados de maneira sensata retornando a Future<Int>, em vez disso automaticamente awaitresultarão e depois chamarão self become: result., o que fará com que o objeto atualmente em execução ( selfou seja, o Future<Int>) se torne literalmente o resultobjeto, ou seja, a partir de agora a referência ao objeto que costumava ser a Future<Int>é agora em Inttodo lugar, completamente transparente para o cliente.

Não é necessário nenhum recurso especial de linguagem relacionada à assincronia.

Jörg W Mittag
fonte
Ok, mas isso tem problemas se ambos Future<T>e Tcompartilhar alguma interface comum e eu uso a funcionalidade dessa interface. Deve becomeo 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.
amon
Entendo que ele não adiciona nenhum recurso, o que importa é que temos diferentes sintaxes para escrever cálculos de resolução imediata e cálculos de longa execução e, depois disso, usaríamos os resultados da mesma maneira para outros fins. Fiquei me perguntando se poderíamos ter uma sintaxe que lide com os dois de forma transparente, tornando-o mais legível e, assim, o programador não precisa lidar com isso. Como fazer a + b, ambos os números inteiros, não importa se aeb estão disponíveis imediatamente ou mais tarde, apenas escrevemos a + b(possibilitando Int + Future<Int>)
Cinn
@Cinn: Sim, você pode fazer isso com o Transparent Futures e não precisa de nenhum recurso especial de idioma para fazer isso. Você pode implementá-lo usando os recursos já existentes em, por exemplo, Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript e, aparentemente, como acabei de ler, Python.
Jörg W Mittag
3
@amon: A idéia do Futuro Transparente é que você não sabe que é um futuro. Do seu ponto de vista, não há interface comum entre Future<T>e Tporque, do seu ponto de vista, não existeFuture<T> , apenas a T. 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.
Jörg W Mittag
3
@ Jörg Parece que isso seria problemático em qualquer coisa, exceto em linguagens funcionais, pois você não tem como saber quando o código é realmente executado nesse modelo. Isso geralmente funciona bem, digamos, Haskell, mas não vejo como isso funcionaria em linguagens procedurais (e mesmo em Haskell, se você se importa com o desempenho, às vezes precisa forçar uma execução e entender o modelo subjacente). Uma ideia interessante, no entanto.
Voo
7

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:

  1. 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:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Três meses depois, um jogador descobre que, ao ser morto e desconectado precisamente quando attacker.addInventoryItemsestá em execução, victim.removeInventoryItemsfalhará, 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.

  2. 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:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    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:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. 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.

user253751
fonte
3

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 Promises 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 ou CompletableFutures 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ção forkIOde 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.

Louis Jackman
fonte
1
Esta foi uma resposta realmente interessante! Mas não sei se entendi sua distinção entre código síncrono e não assíncrono. Para mim, código síncrono sem bloqueio significa que algo como uma função C 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?
amon
1
Uso "assíncrono" e "síncrono" para discutir o modelo de programação exposto ao desenvolvedor e "bloqueio" e "não bloqueio" para discutir o bloqueio de um thread do kernel durante o qual ele não pode fazer nada útil, mesmo se houver outros cálculos que precisam ser feitos e existe um processador lógico sobressalente que ele pode usar. Bem, uma goroutine pode simplesmente esperar um resultado sem bloquear o thread subjacente, mas outra goroutine pode se comunicar com ela através de um canal, se desejar. A goroutine não precisa usar um canal diretamente para aguardar a leitura de um soquete sem bloqueio.
Louis Jackman
Hmm ok, eu entendo sua distinção agora. Enquanto eu estou mais preocupado em gerenciar o fluxo de dados e controle entre corotinas, você está mais preocupado em nunca bloquear o thread principal do kernel. Não tenho certeza se Go ou Haskell têm alguma vantagem sobre C ++ ou Java a esse respeito, pois eles também podem iniciar threads em segundo plano; isso exige apenas um pouco mais de código.
amon
O @LouisJackman poderia elaborar um pouco sobre sua última declaração sobre não-bloqueio assíncrono para programação do sistema. Quais são os profissionais da abordagem assíncrona sem bloqueio?
sunprophit
@sunprophit O não-bloqueio assíncrono é apenas uma transformação do compilador (geralmente assíncrono / aguarda), enquanto o não-bloqueio síncrono requer suporte de tempo de execução, como uma combinação de manipulação de pilha complexa, inserção de pontos de retorno em chamadas de função (que podem colidir com inlining), rastreando " reduções ”(exigindo uma VM como BEAM), etc. Como a coleta de lixo, está diminuindo a complexidade do tempo de execução para facilitar o uso e a robustez. Linguagens de sistemas como C, C ++ e Rust evitam recursos de tempo de execução maiores como esse devido a seus domínios de destino, portanto o não-bloqueio assíncrono faz mais sentido lá.
Louis Jackman
2

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.

monocelular
fonte
2

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.

Mikey Wetzel
fonte
1

O problema que você está descrevendo é duplo.

  • O programa que você está escrevendo deve se comportar de maneira assíncrona como um todo, quando visto de fora .
  • Ele deve não ser visível no site da chamada se uma chamada de função potencialmente dá o controle ou não.

Existem algumas maneiras de conseguir isso, mas elas basicamente se resumem a

  1. tendo vários threads (em algum nível de abstração)
  2. tendo vários tipos de função no nível do idioma, todos chamados assim 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 awaitnã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 fazer awaituma 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:

((x) => {return x + x;})

Eliminação da função síncrona:

f(4)

((x) => {return x + x;})(4)

Você pode contrastar isso com a introdução e eliminação de funções assíncronas.

Introdução à função assíncrona

(async (x) => {return x + x;})

Eliminação de função assíncrona (nota: válida apenas dentro de uma asyncfunção)

await (async (x) => {return x + x;})(4)

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.

> (async (x) => {return x + x;})(4)
Promise { 8 }

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.

Gregory Nisbet
fonte
1

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
Eu quase nunca usei o idioma Go, mas parece que você declara explicitamente go ..., por isso parece semelhante a await ...não?
Cinn
1
@Cinn Na verdade, não. Você pode fazer qualquer chamada como goroutine em sua própria fibra / fio verde com 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.
Deduplicator
2
Embora as Goroutines sejam uma espécie de simultaneidade, eu não as colocaria no mesmo balde que assíncrono / aguardar: não corotinas cooperativas, mas automaticamente (e preventivamente!) Linhas verdes agendadas. Mas isso também não torna a espera automática: o equivalente do Go é a awaitleitura de um canal <- ch.
amon
@amon Até onde eu sei, as goroutines são agendadas cooperativamente em threads nativos (normalmente apenas o suficiente para maximizar o verdadeiro paralelismo de hardware) pelo tempo de execução, e essas são agendadas preventivamente pelo sistema operacional.
Deduplicator
O OP solicitou "poder escrever código assíncrono de maneira síncrona". Como você mencionou, nas goroutines e no go runtime, você pode fazer exatamente isso. Você não precisa se preocupar com os detalhes do encadeamento, apenas escreva o bloqueio de leituras e gravações, como se o código fosse síncrono, e suas outras goroutines, se houver, continuarão em execução. Você também não precisa "aguardar" ou ler em um canal para obter esse benefício. Portanto, acho que o Go é uma linguagem de programação que atende mais de perto aos desejos do OP.
1

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:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Se bar()for uma função assíncrona, pode ser possível obj.xmudar 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:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Agora é claramente visível que bar()é uma função assíncrona, e a maneira correta de lidar com isso é verificar novamente o valor esperado obj.xposteriormente 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.

j_kubik
fonte
0

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:

const nbOfUsers = getNbOfUsers();

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 getNbOfUsersfor assíncrono, então:

const nbOfUsers = await getNbOfUsers();

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.

jcaron
fonte
Você está certo, meu segundo código na pergunta é inválido como se 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.
Cinn 31/03
@Cinn esse não é o meu ponto. O que quero dizer é que o fluxo de execução pode chegar a outras partes do seu código durante a execução da chamada assíncrona, embora não seja possível para uma chamada síncrona. Seria como ter vários threads em execução, mas não estar ciente disso. Isso pode acabar em grandes problemas (que geralmente são difíceis de detectar e reproduzir).
jcaron 31/03
-4

Está disponível no C ++ como std::asyncdesde o C ++ 11.

A função de modelo assíncrona executa a função f de forma assíncrona (potencialmente em um encadeamento separado que pode fazer parte de um conjunto de encadeamentos) e retorna um std :: future que eventualmente reterá o resultado dessa chamada de função.

E com C ++ 20, podem ser usadas corotinas:

Robert Andrzejuk
fonte
5
Isso não parece responder à pergunta. De acordo com o seu link: "O que o TS da Coroutines nos fornece? Três novas palavras-chave no idioma: co_await, co_yield e co_return" ... Mas a pergunta é por que precisamos de uma palavra-chave await(ou co_await, neste caso) em primeiro lugar?
Arturo Torres Sánchez