Skip to main content

Concurrency x Parallelism

  • Concurrency x Parallelism

O que é simultaneidade(concurrency) em JavaScript?

Simultaneidade refere-se à capacidade de gerenciar e executar múltiplas tarefas de forma assíncrona, permitindo que um programa utilize o tempo enquanto espera a conclusão de outras tarefas. Como o JavaScript é de thread único, a simultaneidade dá lugar ao tratamento de tarefas como eventos e operações de E/S de maneira sem bloqueio, garantindo que o thread principal esteja livre para continuar processando as interações do usuário e outras tarefas.

É simplesmente uma maneira de lidar com várias tarefas ao mesmo tempo. Um exemplo do mundo real seria um chef gerenciando vários pratos simultaneamente, alternando para cada um que precisa de atenção. Dessa forma, você pode iniciar, executar e concluir várias tarefas em períodos sobrepostos.

Exemplos de simultaneidade(concurrency) em JavaScript

Existem várias estratégias para executar tarefas simultaneamente, cada uma focada em melhorar o desempenho do sistema através do gerenciamento eficiente de tarefas.

Algumas dessas técnicas são:

  • Retornos de chamada
  • Promessas

Retornos de chamada

Callbacks são funções utilizadas como parâmetros dentro de outras funções, permitindo a execução assíncrona de tarefas. Este mecanismo facilita o tratamento de operações simultâneas, garantindo que outros processos possam prosseguir ininterruptamente enquanto aguardam a conclusão de uma tarefa específica.

Um exemplo fornecido abaixo demonstra o uso eficaz de retornos de chamada para obter simultaneidade.

function fetchData(callback) {
// Simulate a delay to mimic data fetching, like waiting for a server response.
setTimeout(() => {
// Once the 'data' is ready, the callback function is called with the 'data' as an argument.
callback('Data fetched');
}, 1000); // The delay is set to 1000 milliseconds (1 second).
}

// Specify post-data-fetch actions
fetchData((data) => {
// Function to run after data is retrieved
console.log(data); // Display: 'Data fetched'
});

No exemplo acima, fetchDataé uma função que simula a busca de dados de uma fonte remota, o que leva tempo (de forma assíncrona). Costumamos setTimeoutimitar esse atraso. O retorno de chamada é uma função predefinida que é executada automaticamente assim que o processo de recuperação de dados é concluído.

Promessas

Uma promessa é um objeto que encapsula o resultado final de operações assíncronas, oferecendo uma abordagem estruturada e mais eficiente para gerenciar tais tarefas do que os retornos de chamada tradicionais.

Abaixo está um código simples que usa promessas para obter simultaneidade.

function fetchData() {
// fetchData immediately returns a Promise.
return new Promise((resolve) => {
// Inside the Promise, we again simulate a delay in fetching data.
setTimeout(() => {
// Once the data is ready, we 'resolve' the Promise with the data.
resolve('Data fetched');
}, 1000); // The delay is 1000 milliseconds (1 second).
});
}

// fetchData returns a Promise upon call
fetchData().then(data => {
// Use .then() to handle the Promise resolution
console.log(data); // Console output: 'Data fetched'
});

Neste trecho, fetchDatainicia uma operação assíncrona que retorna uma Promise. O .then()método trata o resultado registrando os dados resolvidos no console. Essa abordagem garante um fluxo claro e consistente para gerenciar a recuperação assíncrona de dados.

Prós da simultaneidade(concurrency)

  • Alocação ideal de recursos: a simultaneidade maximiza a utilização de recursos ao lidar com tarefas vinculadas a E/S de maneira eficaz.
  • Capacidade de resposta: melhora a experiência do usuário ao não bloquear o thread principal.
  • Escalabilidade: Facilita o manuseio de um grande número de operações simultaneamente.

Contras da simultaneidade(concurrency)

  • Complexidade: a simultaneidade pode introduzir desafios no gerenciamento do estado e na coordenação de tarefas.
  • Dificuldade de depuração: rastrear e depurar códigos assíncronos pode ser difícil.
  • Dinâmica propensa a erros: O gerenciamento adequado é crucial para evitar condições de corrida e impasses.

O que é paralelismo em JavaScript?

O paralelismo permite a execução simultânea de múltiplas tarefas em vários threads, o que difere da simultaneidade, que gerencia a execução de tarefas de forma sobreposta. É como ter várias abas abertas no seu navegador. Cada guia opera de forma independente e pode realizar tarefas sem esperar que as outras terminem. Em outras palavras, a simultaneidade trata do gerenciamento de muitas coisas ao mesmo tempo, lidando com cada tarefa, uma de cada vez, enquanto o paralelismo normalmente envolve a execução de várias tarefas ao mesmo tempo.

Exemplos de paralelismo em Javascript

Como o JavaScript é de thread único, os seguintes métodos podem produzir paralelismo verdadeiro:

Trabalhadores da Web

Web Workers permitem que você execute JavaScript em threads de segundo plano, separados do thread de execução principal de um aplicativo da web. Com Web Workers, você pode executar tarefas em paralelo sem interromper a interface do usuário.

Abaixo está um código simples que ilustra o uso de Web Workers.

// main.js
// Determine the number of available CPU cores
const cores = navigator.hardwareConcurrency || 4; // Fallback to 4 if hardwareConcurrency is not supported

// Function to initialize and start workers
function startWorkers() {
for (let i = 0; i < cores; i++) {
const worker = new Worker(`worker${i}.js`);
worker.postMessage(`Start working on task ${i}`);

worker.onmessage = function(event) {
console.log(`Worker ${i} says:`, event.data);
};
}
}

// Start the workers
startWorkers();

Para este exemplo, você criaria vários arquivos de trabalho (por exemplo, worker0.jsworker1.js, etc.), cada um contendo uma tarefa que simula um cálculo pesado:

// worker0.js, worker1.js, ..., workerN.js
self.onmessage = function(event) {
console.log(event.data); // Log the received message
const result = performHeavyComputation();
self.postMessage(result);
};

function performHeavyComputation() {
// Simulate a heavy computation task
let sum = 0;
for (let i = 0; i < 1e8; i++) {
sum += Math.sqrt(i);
}
return `Task completed with the result: ${sum}`;
}

O código irá gerar mensagens de log do console em ambos os threads. No thread principal, uma mensagem de log confirmará a ativação de um trabalhador. Nas threads de trabalho, as mensagens sinalizarão o recebimento do comando de início e a conclusão da tarefa de computação intensiva.

Aqui está um exemplo de como seria a saída do console:

Worker 0 says: Task completed with the result: [sum from worker 0]
Worker 1 says: Task completed with the result: [sum from worker 1]
...
Worker N says: Task completed with the result: [sum from worker N]

Cada um [sum from worker X]representa a soma das raízes quadradas dos números de 0 a (1e8) calculados por cada trabalhador. A soma dependeria do cálculo realizado pela performHeavyComputationfunção de cada arquivo de trabalho.

Quando um sistema possui vários núcleos de CPU, os Web Workers os utilizam para processamento paralelo. Cada trabalhador opera em um núcleo individual, alcançando um verdadeiro paralelismo e permitindo que tarefas como cálculos complexos sejam executadas simultaneamente, aumentando assim a eficiência.

Porém, se o número de trabalhadores exceder o número de núcleos de CPU disponíveis, o navegador gerenciará sua execução. Embora ainda possam ser executados simultaneamente, eles não funcionarão em verdadeiro paralelismo. Este cenário pode levar a problemas de desempenho devido à necessidade de alternância de contexto e à sobrecarga adicional associada ao gerenciamento de vários trabalhadores em menos núcleos.

Por exemplo, em um sistema com oito núcleos de CPU, a criação de oito threads de trabalho pode permitir que tarefas sejam executadas simultaneamente, potencialmente em núcleos separados. Porém, é importante lembrar que o thread principal, que cria os trabalhadores, também requer tempo de CPU. Portanto, na prática, pode-se criar menos de oito trabalhadores para garantir que o thread principal possa operar de forma eficiente sem contenção de recursos.

Também vale a pena saber que embora seja tecnicamente viável criar um grande número de Web Workers, como 100, geralmente não é aconselhável porque pode levar a problemas de desempenho. Cada web trabalhador é executado em seu thread, e ter muitos pode levar ao alto uso de memória e ao aumento da complexidade no gerenciamento deles. É melhor equilibrar a necessidade de processamento paralelo com os recursos de sistema disponíveis e a sobrecarga do gerenciamento de vários trabalhadores.

Paralelo.js

Parallel.js é uma biblioteca que fornece uma abstração sobre Web Workers para realizar operações em paralelo. Ele fornece uma maneira mais fácil de executar processamento paralelo, abstraindo Web Workers, facilitando a execução de funções em paralelo. Para usar esta biblioteca, normalmente você a importaria e usaria sua API para executar tarefas em paralelo.

Abaixo está um exemplo de seu uso para obter paralelismo.

// Importing parallel.js
const Parallel = require('paralleljs');
// Create a new Parallel object with an array of data.
const p = new Parallel([1, 2, 3, 4]);

// Use the .map() method to operate on each element in parallel.
p.map(number => number * 2).then(data => {
// Once the parallel operation is complete, .then() is called with the results.
console.log(data); // Expected output: [2, 4, 6, 8]
});

No exemplo acima, criamos um Parallelobjeto e o atribuímos a um array de números. Em seguida, dobramos cada número usando o .map()método. A operação é executada em paralelo e os resultados são exibidos quando concluídos.

Web Workers e Parallel.js lidam com tarefas computacionalmente intensivas sem bloquear o thread principal, mantendo assim a capacidade de resposta da aplicação web. Web Workers exigem que você mesmo gerencie os threads separados e a comunicação, enquanto a biblioteca oferece uma API de nível superior para abstrair parte dessa complexidade. Cada um tem seus casos de uso dependendo da complexidade da tarefa e do nível de controle necessário sobre a execução paralela.

Prós do paralelismo

  • Desempenho: o paralelismo pode acelerar significativamente tarefas que exigem uso intensivo da CPU.
  • Sem bloqueio: as operações não bloqueiam o thread principal, mantendo a capacidade de resposta do aplicativo.
  • Utilização de recursos: Faz uso eficaz de processadores multi-core.

Contras do paralelismo

  • Consumo de recursos: A execução de tarefas em paralelo pode consumir mais recursos do sistema, como CPU e memória, especialmente ao lidar com inúmeras tarefas simultâneas.

  • Suporte ao navegador: os navegadores modernos geralmente fornecem funcionalidade para Web Workers, permitindo que o JavaScript seja executado em threads em segundo plano. No entanto, nem todos os navegadores suportam isso. É crucial verificar a compatibilidade do navegador para garantir a funcionalidade em diferentes ambientes de usuário. Para obter as informações mais atuais de suporte do navegador para Web Workers, consulte as tabelas de suporte fornecidas pelo site “Posso usar…”

    . Este recurso oferece detalhes abrangentes e atualizados sobre a compatibilidade do navegador para uma ampla variedade de recursos da web.

  • Depuração e teste: a depuração de códigos paralelos pode ser mais desafiadora do que o código sequencial.

Conclusão

Simultaneidade e paralelismo são conceitos centrais em JavaScript necessários para garantir a eficiência e o desempenho do sistema. Embora ambos tenham desvantagens, focar nas suas vantagens beneficiará o desenvolvimento de aplicações web eficientes.

Lembre-se, a simultaneidade é o gerenciamento de múltiplas tarefas executando-as uma de cada vez, muitas vezes sobrepostas, enquanto o paralelismo é a execução de múltiplas tarefas simultaneamente. Embora o paralelismo vise maximizar o desempenho aproveitando os recursos de hardware de forma eficiente, a simultaneidade tem mais a ver com o gerenciamento da complexidade das tarefas de maneira multitarefa.

JavaScript, sendo uma linguagem de thread único, alcança paralelismo usando técnicas como Web Workers e a biblioteca Parallel.js, conforme discutido anteriormente neste artigo. A simultaneidade, por outro lado, é tratada por meio de modelos de programação assíncronos, como retornos de chamada e promessas. A utilização eficaz de ambos os conceitos pode levar ao desenvolvimento de aplicações JavaScript responsáveis.