Executando verificação de segurança...
4

Acho que a discussão precisa começar com a definição do que significa ser assíncrono. Me parece que todos os comentários abaixo estão falando sobre o mesmo assunto, mas discordam sobre o que significa ser assíncrono. A especificação do ECMAScript (o nome oficial do JavaScript) menciona suas capacidades para desenvolver funções assíncronas, bem como para interagir com elas. https://tc39.es/ecma262/ Então, talvez a discussão aqui deva ser sobre o que uma linguagem precisa ter para ser considerada assíncrona.

Gostaria de pedir que o autor, ou outros que defendem que ela não é ou não suporta funções assíncronas, apresentem:

  1. A definição do que significa ser assíncrono e
    1. Um exemplo de uma outra linguagem que é considerada assíncrona e que faz algo que não pode ser reproduzido em JavaScript com suas bibliotecas padrão.

Acho que isso ajudaria no entendimento.

Obrigado

Carregando publicação patrocinada...
2

Cristian, claramente o autor do post está confuso no conceito do que é ser assíncrono.
A definição foi colocada explicitamente ou implicitamente em outros comentários, não tem muito o que discutir.

O que eu acho que ele realmente está querendo dizer é sobre concorrência ou paralelismo na execução das threads.

No entanto, o Javascript (que quer dizer mais sobre sintaxe aqui) nem deveria entrar em discussão, pois seguindo a especificação do ECMAScript que você mesmo compartilhou, há os "motores" responsáveis por implementar isso e aí temos uma gama completa de implementações para usos diferentes.

O que eu tentei argumentar com o autor é que ele está discutindo algo que conceitualmente estava errado desde o início, todo o resto é consequência disso.

1

Um exemplo de uma outra linguagem que é considerada assíncrona e que faz algo que não pode ser reproduzido em JavaScript com suas bibliotecas padrão.

Goroutines, são assincronas de verdade já que são concorrentes, são single-threads e não podem ser reproduzidos em JavaScript com suas bibliotecas padrão apenas emuladas o que torna o desempenho incrívelmente lento

1

Eu programo em Go, e as goroutines não são executadas em uma única thread. Aqui está a documentação oficial do Go: https://go.dev/doc/effective_go. O segundo parágrafo da seção sobre goroutines:

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

Esse trecho explica que as goroutines são distribuídas em múltiplas threads do sistema operacional.

1

Também programo em Go e a real é que depende, Go é inteligente, ele divide as gorrotinas se necessário apenas, você pode ter 100 gorrotinas se nessas 100 nenhuma bloquear a thread principal, go não cria novas thread, inclusive você pode testar com watch ps -o thcount <pid> dependendo de como o código é estruturado esse número não aumenta

1

Mano, tu tá insistindo em atribuir concorrência à assincronia. Isso está errado.

Você pode até achar que seja melhor que a função assíncrona funcione em paralelo, ou concorrentemente. Mas isso é só a sua opinião. Isso não faz parte do conceito de assíncronia e jamais fará. São conceitos e utilidades diferentes.

Pegue qualquer linguagem que você considere "realmente assíncrona" e coloque para rodar em um processador com um único núcleo de processamento (ou limite o acesso do programa a apenas um núcleo). Você já não consegue mais que o programa seja "realmente concorrente", porque o processador vai executar uma instrução por vez, uma atrás da outra, mesmo que a linguagem simule uma concorrência. Mas ainda assim você pode ter a assincronia, independentemente de quantos núcleos o programa tenha acesso. Porque a assincronia só depende da instrução ser executada (ou resolver) fora da ordem que foi deflagrada.

Segundo a sua definição, um programa "realmente assíncrono" deixaria de sê-lo se ficar limitado a um único núcleo de processamento, mas isso não é verdade, pelo contrário, a única coisa que acontece é que o programa deixa de ser "realmente concorrente".

Veja bem esse exemplo:

let someCounter = 0;
const asyncFunction = async () => await new Promise(resolve => setTimeout( ()=>resolve(someCounter++), 500) );
const theValues =
{
    // Recebe 1
    async: asyncFunction().then( value => theValues.async = value ),
    
    // Recebe 0
    sync: someCounter++,
    
    // Mostra 2
    get counter () { return someCounter },
}
setTimeout( () => console.log(theValues), 500 );

A propriedade async é claramente definida antes da propriedade sync, porém esta última recebe 0 do contador enquanto a primeira recebe 1 do contador.

Por que isso acontece? Porque a propriedade async resolve seu valor somente 500ms depois da propriedade sync.

Por mais que o JavaScript tenha uma pilha de execução que ocorre em ordem, isso não impede a assincronia. Faz parte do design da linguagem para manter a ordem. Se você tem dois timers de 1ms definidos imediatamente um após o outro, porque diabos seria interessante que o timer definido por último execute antes do que foi definido primeiro? Na minha opinião, o JavaScript faz muito bem em manter as coisas em ordem.

1

Sim, elixir/Erlang, go, v todas possuem greenthreads roda em uma única cpu, porém roda assíncronamente.

Tenho duas fontes, uma com um exemplo dado por Fabio Akita em vídeo: The Elixir of Life por Fabio Akita - DevInSantos 2015

E a outra é uma pergunta feita no site o Elixir: How are Elixir processes lightweight?

Não é que sou hate de javascript não, tem seus pontos negativos, mas não é um problema pra mim, além de ter me feito sentido o que foi dito no post original eu lembrei desse vídeo acima onde explicava isso.

E agradeço por sua forma serena de alcamar os animos de todos.

2

Obrigado pela resposta! O vídeo me ajudou a compreender melhor. Acho que a confusão é em distinguir entre multithreading e assincronia.

No vídeo, se eu entendi direito, ele menciona que Elixir é multithread e, como você disse, usa green threads. Já o JavaScript não é multithread, mas isso não significa que não seja assíncrono.

No exemplo do vídeo, ele é injusto com o JavaScript, pois utiliza uma função sleep bloqueante, que nao é parte da linguagem,mas uma biblioteca que ele installou, enquanto em Elixir ele usa uma função não bloqueante (de acordo com ele mesmo). Se ele usasse uma função bloqueante em Elixir e forçasse a execução em uma única thread, teria o mesmo problema.

Se esse é o caso e o JavaScript executa código assíncrono, então, desde que a função seja não bloqueante como a do exemplo em Elixir, deveria ser possível reproduzir o mesmo comportamento e realizar 400 chamadas em 4 segundos, correto? Decidi tentar! :)

Abaixo está a minha reprodução do código do vídeo usando a mesma função bloqueante.

var express = require("express");
var router = express.Router();

router.get("/", function (req, res) {
  require("sleep").sleep(1);
  res.json({ message: "Hello World!" });
});

module.exports = router;

function startServer() {
  var app = express();
  app.use(router);
  app.listen(3002);
  console.log("Server started at http://localhost:3002");
}

startServer();

E aqui está o resultado

Javascript Async siege -r 1 -c 10 http://localhost:3002
** SIEGE 4.1.6
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /
HTTP/1.1 200     2.03 secs:      26 bytes ==> GET  /
HTTP/1.1 200     3.03 secs:      26 bytes ==> GET  /
HTTP/1.1 200     4.04 secs:      26 bytes ==> GET  /
HTTP/1.1 200     5.05 secs:      26 bytes ==> GET  /
HTTP/1.1 200     6.05 secs:      26 bytes ==> GET  /
HTTP/1.1 200     7.05 secs:      26 bytes ==> GET  /
HTTP/1.1 200     8.06 secs:      26 bytes ==> GET  /
HTTP/1.1 200     9.06 secs:      26 bytes ==> GET  /
HTTP/1.1 200    10.07 secs:      26 bytes ==> GET  /

Transactions:                     10 hits
Availability:                 100.00 %
Elapsed time:                  10.07 secs
Data transferred:               0.00 MB
Response time:                  5.55 secs
Transaction rate:               0.99 trans/sec
Throughput:                     0.00 MB/sec
Concurrency:                    5.51
Successful transactions:          10
Failed transactions:               0
Longest transaction:           10.07
Shortest transaction:           1.02

Usando uma função bloqueante, consegui reproduzir a mesma lentidão mostrada no vídeo.

Agora, veja o que acontece se eu usar uma função sleep que não bloqueia:

var express = require("express");
var router = express.Router();

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

router.get("/", function (req, res) {
  //   require("sleep").sleep(1);
  sleep(1000).then(() => {
    res.json({ message: "Hello World!" });
  });
});

module.exports = router;

function startServer() {
  var app = express();
  app.use(router);
  app.listen(3002);
  console.log("Server started at http://localhost:3002");
}

startServer();

Resultado:

(base) ➜  Javascript Async siege -r 1 -c 10 http://localhost:3002 
** SIEGE 4.1.6
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /

Transactions:                     10 hits
Availability:                 100.00 %
Elapsed time:                   1.02 secs
Data transferred:               0.00 MB
Response time:                  1.01 secs
Transaction rate:               9.80 trans/sec
Throughput:                     0.00 MB/sec
Concurrency:                    9.93
Successful transactions:          10
Failed transactions:               0
Longest transaction:            1.02
Shortest transaction:           1.01

Observe que cada requisição levou cerca de 1 segundo, e o tempo total foi 1.02 segundos.

Posso até reproduzir o teste mais pesado do vídeo, com 2 repetições e 200 usuários paralelos:

(base) ➜  Javascript Async siege -r 2 -c 200 http://localhost:3002
** SIEGE 4.1.6
** Preparing 200 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.01 secs:      26 bytes ==> GET  /
....
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /
HTTP/1.1 200     1.02 secs:      26 bytes ==> GET  /

Transactions:                    400 hits
Availability:                 100.00 %
Elapsed time:                   2.08 secs
Data transferred:               0.01 MB
Response time:                  1.03 secs
Transaction rate:             192.31 trans/sec
Throughput:                     0.00 MB/sec
Concurrency:                  197.96
Successful transactions:         400
Failed transactions:               0
Longest transaction:            1.06
Shortest transaction:           1.00

Note como as requisições individuais continuam em torno de 1 segundo, e o total é 2.08 segundos, mais rápido que Elixir no vídeo.

Com relação ao primeiro ponto, o JavaScript roda código assíncrono e consegue responder a chamadas em paralelo, mesmo usando uma única thread. No entanto, é importante lembrar que, se o código for escrito de forma síncrona, ele não será executado de maneira assíncrona. Cabe ao desenvolvedor garantir que o código seja escrito para rodar assincronamente.

Quando o JavaScript encontra funções assíncronas por natureza, como operações de I/O, ele retorna a execução para o fluxo principal do código, permitindo que outras instruções continuem sem esperar. Porém, se o código nunca chegar a uma função assíncrona, ele será executado de forma totalmente síncrona. No meu código acima, essa função assíncrona é o setTimeout. Em algumas linguagem como Python, o desenvolvedor tem que fazer isto explicitamente.

JavaScript se destacou justamente por facilitar a execução de funções de I/O, como leitura de arquivos e chamadas de rede de maneira assíncrona, permitindo que o código continue avançando sem esperar pela resposta do servidor remoto.

O que JavaScript não faz é executar código em paralelo. Todo o código roda em uma única thread e em um event loop. Portanto, se houver algo pesado para processar, como uma grande quantidade de dados, o JavaScript ficará limitado pela capacidade dessa única thread.

Me fale se o que escrevi faz sentido. Estou curioso para saber se consegui entender seu ponto e se estamos alinhados na diferença entre assíncrono e paralelo.

1

Cristian, macho eu criei alguns exemplos pra te dá sobre greenthread e goroutines e testei iria trazer o exemplo dos resultados quando eu percebi que o meu conhecimento de relacionado a threads estava fraco, pois acabava que não batia quando realizava o teste era muita teoria, e teoria equivocada.

E reconheço a minha confusão, peço desculpas e obrigado por sua paciência em explicar.