🔵 ПОТОКИ И ПАМЯТЬ В JS. ЧАСТЬ 1.
Наверное, многие из вас слышали, что все объектные типы в JS передаются по ссылке, а остальные – по значению. В общем то, если не докапываться до нюансов, то на уровне базового понимания оно почти так и есть.
Почему почти? 🤔
Ну, дело в том, что в нашем окружении может быть API для создания потоков, как, например, WebWorker в браузере или worker_threads в Node.js. А раз у нас появляются потоки и параллельные вычисления, то неизбежно всплывает проблема гонок данных и модель памяти.
Как же с этим борются в JS?
Ну, самое простое, что можно сделать – это отказаться от разделяемого изменяемого состояния. Если его нет, то и нет связанных с ним проблем. Как этого добиться? Ну, просто при передаче данных из потока в поток – копировать. У каждого потока будет своя независимая копия и больше никаких гонок. В общем не буду тянуть резину – по умолчанию в JS так и делается.
По умолчанию, при передаче объекта из потока в поток он копируется используя специальный алгоритм структурного копирования – https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm.
Кстати, у нас также становиться доступна глобальная функция structuredClone и мы можем использовать её для глубокого копирования объектов.
Но так или эффективно всегда копировать? 🤨
Конечно, если объем передаваемых данных невелик, то копирование будет работать очень быстро. Но что, если мы передаем, скажем некоторое изображение или видео, которые вполне могут весить десятки и даже сотни мегабайт? Копирование такого объема может стать весьма и весьма затратным. 🫤
Как же быть?🧐
Решение есть, и даже несколько.
👉🏻Во-первых, в JS есть такой признак у объекта, как Transferable.
Он означает, что этот объект может безопасно передаваться из потока в поток без каких-либо копирований, т. к. по контракту у такого объекта может быть только один «хозяин». Говоря в терминологии Rust, при такой передаче мы передаем владение объектом.
🎮Проведем простую аналогию: вы живете со своим братом в одной квартире и у вас есть игровая приставка и оба любите играть. Но, к сожалению, любите играть в разное. Т. к. для вас приставка разделяемый изменяемый ресурс, то у вас образуется ситуация гонки. Копировать, т. е. купить новую приставку для вас дорого, поэтому вы договариваетесь играть по очереди. Т. е. вы договариваетесь передавать владение приставкой друг другу на время, и каждый волен играть в свой время во что угодно. Но, разумеется, пока ваш брат владеет приставкой вы играть не можете, т. к. фактически вам не на чем играть. Гонок больше нет.
Но как же настроить передачу данных в JS, чтобы они передавали владение. 🤔
🔹Ну, во-первых, для этого наши данные должны обладать признаком Transferable. Такой признак есть у некоторых встроенных объектов, например, ArrayBuffer или WritableStream. Самостоятельно задавать такой признак мы не можем.
🔹А во-вторых, нам нужно указать доп. параметром в метод postMessage или structuredClone какие объекты мы хотим передать по владению.
Разумеется, такой способ передачи очень эффективный, т.к. фактически никаких копирований не делается. Но теперь нам нужно подстраиваться, что в один момент времени у данных может быть только один хозяин.
👉🏻Но я уже упоминал, что есть более одного способа решить эту проблему и это действительно так.
Дело в том, что в JS есть еще такая структура данных, который может использоваться одновременно из разных потоков – это SharedArrayBuffer. Но про то, как его можно использовать и какие плюсы и минусы этого подхода я расскажу в своём следующем посте.
🔵 Если тебе интересна тема параллельных вычислений и конкурентного доступа к данным в JS, то приглашаю на свой авторский курс CS во Frontend – https://kobezzza.ru/.
Там мы детально разберем все эти аспекты и научимся применять их на практике.
Наверное, многие из вас слышали, что все объектные типы в JS передаются по ссылке, а остальные – по значению. В общем то, если не докапываться до нюансов, то на уровне базового понимания оно почти так и есть.
Почему почти? 🤔
Ну, дело в том, что в нашем окружении может быть API для создания потоков, как, например, WebWorker в браузере или worker_threads в Node.js. А раз у нас появляются потоки и параллельные вычисления, то неизбежно всплывает проблема гонок данных и модель памяти.
Как же с этим борются в JS?
Ну, самое простое, что можно сделать – это отказаться от разделяемого изменяемого состояния. Если его нет, то и нет связанных с ним проблем. Как этого добиться? Ну, просто при передаче данных из потока в поток – копировать. У каждого потока будет своя независимая копия и больше никаких гонок. В общем не буду тянуть резину – по умолчанию в JS так и делается.
По умолчанию, при передаче объекта из потока в поток он копируется используя специальный алгоритм структурного копирования – https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm.
Кстати, у нас также становиться доступна глобальная функция structuredClone и мы можем использовать её для глубокого копирования объектов.
Но так или эффективно всегда копировать? 🤨
Конечно, если объем передаваемых данных невелик, то копирование будет работать очень быстро. Но что, если мы передаем, скажем некоторое изображение или видео, которые вполне могут весить десятки и даже сотни мегабайт? Копирование такого объема может стать весьма и весьма затратным. 🫤
Как же быть?🧐
Решение есть, и даже несколько.
👉🏻Во-первых, в JS есть такой признак у объекта, как Transferable.
Он означает, что этот объект может безопасно передаваться из потока в поток без каких-либо копирований, т. к. по контракту у такого объекта может быть только один «хозяин». Говоря в терминологии Rust, при такой передаче мы передаем владение объектом.
🎮Проведем простую аналогию: вы живете со своим братом в одной квартире и у вас есть игровая приставка и оба любите играть. Но, к сожалению, любите играть в разное. Т. к. для вас приставка разделяемый изменяемый ресурс, то у вас образуется ситуация гонки. Копировать, т. е. купить новую приставку для вас дорого, поэтому вы договариваетесь играть по очереди. Т. е. вы договариваетесь передавать владение приставкой друг другу на время, и каждый волен играть в свой время во что угодно. Но, разумеется, пока ваш брат владеет приставкой вы играть не можете, т. к. фактически вам не на чем играть. Гонок больше нет.
Но как же настроить передачу данных в JS, чтобы они передавали владение. 🤔
🔹Ну, во-первых, для этого наши данные должны обладать признаком Transferable. Такой признак есть у некоторых встроенных объектов, например, ArrayBuffer или WritableStream. Самостоятельно задавать такой признак мы не можем.
🔹А во-вторых, нам нужно указать доп. параметром в метод postMessage или structuredClone какие объекты мы хотим передать по владению.
// 16MB
const uInt8Array = Uint8Array.from({length: 1024 * 1024 * 16}, (v, i) => i);
const transferred = structuredClone(uInt8Array, {
transfer: [uInt8Array.buffer],
});
console.log(uInt8Array.byteLength); // 0
console.log(transferred.byteLength); // 16777216
Разумеется, такой способ передачи очень эффективный, т.к. фактически никаких копирований не делается. Но теперь нам нужно подстраиваться, что в один момент времени у данных может быть только один хозяин.
👉🏻Но я уже упоминал, что есть более одного способа решить эту проблему и это действительно так.
Дело в том, что в JS есть еще такая структура данных, который может использоваться одновременно из разных потоков – это SharedArrayBuffer. Но про то, как его можно использовать и какие плюсы и минусы этого подхода я расскажу в своём следующем посте.
🔵 Если тебе интересна тема параллельных вычислений и конкурентного доступа к данным в JS, то приглашаю на свой авторский курс CS во Frontend – https://kobezzza.ru/.
Там мы детально разберем все эти аспекты и научимся применять их на практике.