O objeto Atomics do JS uma introdução, multithreading e memória!

urielsouza29

Uriel dos Santos Souza

Posted on July 15, 2023

O objeto Atomics do JS uma introdução, multithreading e memória!

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:

Window: postMessage() method - Web APIs | MDN

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

favicon developer.mozilla.org

O navegador usa o algoritmo structured clone

The structured clone algorithm - Web APIs | MDN

The structured clone algorithm copies complex JavaScript objects. It is used internally when invoking structuredClone(), to transfer data between Workers via postMessage(), storing objects with IndexedDB, or copying objects for other APIs.

favicon developer.mozilla.org

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:

ArrayBuffer - JavaScript | MDN

O objeto ArrayBuffer é um tipo de dado usado para representar um genérico, buffer de dados binários de tamanho fixo. Você não pode manipular diretamente os conteúdos de um ArrayBuffer; em vez disso, você cria um objeto ArrayBufferView que representa o buffer em um formato específico, e usa para ler e escrever os conteúdos do buffer.

favicon developer.mozilla.org

Mas temos o problema, assim que for transferido o main perde ele.
É melhor que postMessage sim.

Arraybuffer objeto transferivel:

Transferable objects - Web APIs | MDN

Transferable objects are objects that own resources that can be transferred from one context to another, ensuring that the resources are only available in one context at a time. Following a transfer, the original object is no longer usable; it no longer points to the transferred resource, and any attempt to read or write the object will throw an exception.

favicon developer.mozilla.org

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:

Broadcast Channel API - Web APIs | MDN

The Broadcast Channel API allows basic communication between browsing contexts (that is, windows, tabs, frames, or iframes) and workers on the same origin.

favicon developer.mozilla.org

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);

Enter fullscreen mode Exit fullscreen mode

O que é Int32Array:

Int32Array - JavaScript | MDN

The Int32Array typed array represents an array of twos-complement 32-bit signed integers in the platform byte order. If control over byte order is needed, use DataView instead. The contents are initialized to 0. Once established, you can reference elements in the array using the object's methods, or using standard array index syntax (that is, using bracket notation).

favicon developer.mozilla.org

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:

Atomics - JavaScript | MDN

The Atomics namespace object contains static methods for carrying out atomic operations. They are used with SharedArrayBuffer and ArrayBuffer objects.

favicon developer.mozilla.org

// 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);

Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Todos os códigos são retirados deste texto:

The Return of SharedArrayBuffers and Atomics - SitePen

See how we leverage SharedArrayBuffers to share memory between various thread contexts while avoiding race conditions using the Atomics methods.

favicon sitepen.com

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

https://www.youtube.com/watch?v=2w7Ewkpftd4

💖 💪 🙅 🚩
urielsouza29
Uriel dos Santos Souza

Posted on July 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related