O objeto Atomics do JS uma introdução, multithreading e memória!
Uriel dos Santos Souza
Posted on July 15, 2023
Nosso querido JavaScript é single threaded(como todos sempre pensamos, embora isso não esteja na especificação da linguagem JS).
Java Script só pode executar uma coisa por ve sendo single threaded, guarde isso, isso é importante!
Portanto, se uma tarefa de execução longa bloquear o encadeamento principal do navegador, o navegador terá dificuldade para renderizar.
Por exemplo, se fôssemos percorrer e executar operações em uma grande estrutura de dados,
ou criar um loop simples muito grande.
Isso poderia fazer com que a renderização congelasse. Enquanto o loop não terminasse, ou enquanto o algoritmo estivesse percorrendo a grande estrutura de dados, não da pra fazer nada, apenas olhar e esperar.
Esse é um problema de single threaded.
Só da pra fazer uma coisa por vez e se ela toma muito tempo, não da pra fazer mais nada!
Não iremos falar sobre callbacks e promises neste texto.
Uma das soluções para esse problema é usar Web Workers.
Web Workers são mecanismos que permitem que uma operação de um dado script seja executado em uma thread diferente da thread principal da aplicação Web. Permitindo que cálculos laboriosos sejam processados sem que ocorra bloqueio da thread principal (geralmente associado à interface).
Fonte > mdn.
Sabemos que js é single threaded.
Com o tempo as aplicações no navegador estavam ficando cada vez mais exigentes.
Então resolveram trazer os web Workers para nos ajudar!
Agora aquele loop gigante pode ir pra outro thread e deixa nossa tela principal linda e leve sem ninguém atrapalhar!
Os Web Workers permitem a execução paralela no contexto do navegador.
No entanto como tudo em tecnologia, os Web Workers têm suas desvantagens; por exemplo, há um custo de transferência de dados de e para o thread Worker. Toda transferência é feita via postMessage.
O que é postMessage:
O navegador usa o algoritmo structured clone
Para duplicar um objeto, ou seja duplicação dos mesmos dados na memoria. Esse custo, dependendo das circunstâncias, pode superar o benefício da transferência para o thread Worker.
Você também pode transferir array Buffer!
O que é array Buffer:
Mas temos o problema, assim que for transferido o main perde ele.
É melhor que postMessage sim.
Arraybuffer objeto transferivel:
Mas nosso foco não é array buffer também!
Você tem uma outra opção que é a Broadcast Channel API
O que é Broadcast Channel API:
Mas ela tem o mesmo problema de mensagens!
E usa o mesmo algoritmo de clone o structured clone e claro usa PostMessage!
E se pudéssemos evitar a cópia ou transferência de dados?
Uma maneira de contornar esses problemas é aproveitar SharedArrayBuffer.
A partir daqui podemos afirmar que JavaScript não é mais single threaded por padrão!
Web workers não fazem parte do javascript em si. Não estão na especificação da linguagem.
Mas estão em outras especificações(que não vem ao caso agora)
Para desenvolvedores de JavaScript, essa construção nos permite criar pedaços de memoria compartilhada. Em vez de copiar os dados do thread principal para o Worker e vice-versa, podemos atualizar a mesma memória compartilhada de ambos os lados.
//
const length = 10;
// Criando o tamanho do nosso buffer
const size = Int32Array.BYTES_PER_ELEMENT * length;
// Criando o buffer com 10 inteiros
const sharedBuffer = new SharedArrayBuffer(size);
const sharedArray = new Int32Array(sharedBuffer);
O que é Int32Array:
O que é sharedarraybuffer:
Agora temos um pedaço de memória que pode ser compartilhado. Ou seja, o thread principal e os web workers podem acessar esse mesmo local juntos!
Isso é muito bom, economiza memória!
Mas como tudo em tecnologia, tem desvantagens!
Imagina o seguinte: Temos 3 ou mais Web Workers além do thread principal acessando um mesmo local de memória. Isso vai dar errado.
Muito errado. Podemos ter uma condição de corrida. Todos disputando o mesmo lugar ou termos um problema de atualizar 2 vezes o mesmo lugar com as mesmas informações.
Ou seja 2 workers podem fazer o mesmo trabalho ao mesmo tempo e isso é desperdício.
O que é condição de corrida:
A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.
Então criaram o objeto Atomics.
As operações atômicas garantem que tenhamos uma maneira padronizada de ler e gravar dados em uma ordem previsível
Atomics gerencia o acesso de todo mundo ao pedaço de memória, assim impedindo condições de corrida, trabalhos desperdiçado, etc.
O que é Atomics:
// main.js
const worker = new Worker('worker.js');
const length = 10;
const size = Int32Array.BYTES_PER_ELEMENT * length;
const sharedBuffer = new SharedArrayBuffer(size);
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < 10; i++) {
Atomics.store(sharedArray, i, 0);
}
worker.postMessage(sharedBuffer);
Acessando o dados no worker
// worker.js
self.addEventListener('message', (event) => {
const sharedArray = new Int32Array(event.data);
for (let i = 0; i < 10; i++) {
const arrayValue = Atomics.load(sharedArray, i);
console.log(`The item at array index ${i} is ${arrayValue}`);
}
}, false);
E se quiséssemos atualizar a matriz do trabalhador? Temos duas opções para essas atualizações usando o Atomics. Podemos usar storeo que vimos antes, ou podemos usar exchange. A diferença aqui é que storeretorna o valor que é armazenado e exchangeretorna o valor que é substituído. Vamos ver como isso funciona na prática:
// worker.js
self.addEventListener('message', (event) => {
const sharedArray = new Int32Array(event.data);
for (let i = 0; i < 10; i++) {
if (i%2 === 0) {
const storedValue = Atomics.store(sharedArray, i, 1);
console.log(`The item at array index ${i} is now ${storedValue}`);
} else {
const exchangedValue = Atomics.exchange(sharedArray, i, 2);
console.log(`The item at array index ${i} was ${exchangedValue}, now 2`);
}
}
}, false);
Agora podemos ler e atualizar a matriz do thread principal e do thread de trabalho. Atomics tem alguns outros métodos que podemos usar a nosso favor para gerenciar nossos novos arrays compartilhados. Dois dos métodos mais úteis são waite wake. waitnos permite esperar uma mudança em um índice de array e então continuar com as operações. Na prática, isso pode parecer algo assim no lado do trabalhador:
self.addEventListener('message', (event) => {
const sharedArray = new Int32Array(event.data);
const arrayIndex = 0;
const expectedStoredValue = 50;
// An optional 4th argument can be passed which is a timeout
Atomics.wait(sharedArray, arrayIndex, expectedStoredValue);
// Log the new value
console.log(Atomics.load(sharedArray, arrayIndex));
}, false);
Aqui estamos esperando uma alteração em arrayIndex0, onde o valor armazenado esperado é 50. Então, podemos dizer a ele para acordar do thread principal quando alterarmos o valor no índice:
const newArrayValue = 100;
Atomics.store(sharedArray, 0, newArrayValue);
// The index that is being waited on
const arrayIndex = 0;
// The first agent waiting on the value
const queuePos = 1;
Atomics.wake(sharedArray, arrayIndex, queuePos);
Outras funções são fornecidas por conveniência, como adde subque adicionam ou subtraem do índice da matriz, respectivamente. Se você estiver interessado em operações bit a bit , algumas delas são fornecidas, incluindo or, ande xor.
Podemos ver que atomics podemos evitar condições de corrida e fazer atualizações previsíveis em um array usando os métodos de Atomics.
Para usar strings, precisamos convertê-las em uma representação numérica, por exemplo, um padrão conhecido para codificação como UTF-16.
function sharedArrayBufferToUtf16String(buf) {
const array = new Uint16Array(buf);
return String.fromCharCode.apply(null, array);
}
function utf16StringToSharedArrayBuffer(str) {
// 2 bytes for each char
const bytes = str.length *2;
const buffer = new SharedArrayBuffer(bytes);
const arrayBuffer = new Uint16Array(buffer);
for (let i = 0, strLen = str.length; i < strLen; i++) {
arrayBuffer[i] = str.charCodeAt(i);
}
return { array: arrayBuffer, buffer: buffer };
}
const exampleString = "Hello world, this is an example string!";
const sharedArrayBuffer = utf16StringToSharedArrayBuffer(exampleString).buffer;
const backToString = sharedArrayBufferToUtf16String(sharedArrayBuffer);
Todos os códigos são retirados deste texto:
Fiz uma pequena introdução ao objeto Atomics
lembrando que tudo isso *funciona no Node também! *
Se você quiser saber mais basta ler as fontes:
https://www.sitepen.com/blog/the-return-of-sharedarraybuffers-and-atomics
https://dev.to/feezyhendrix/worker-threads-in-node-js-2ikh
https://exploringjs.com/es2016-es2017/ch_shared-array-buffer.html
https://blogtitle.github.io/using-javascript-sharedarraybuffers-and-atomics/
https://blog.logrocket.com/understanding-sharedarraybuffer-and-cross-origin-isolation/
https://www.tutorialspoint.com/what-is-the-use-of-atomics-in-javascript
https://webreflection.medium.com/about-sharedarraybuffer-atomics-87f97ddfc098
https://www.geeksforgeeks.org/atomics-in-javascript/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics
Posted on July 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.