Про ArrayBuffers и SharedArrayBuffers в картинках (часть 2)

186

В прошлой статье я объяснял, как языки, подобные JavaScript, работают с памятью. Я также рассказал, как работают языки с ручным управлением памятью вроде С.
Какое отношение это имеет к объектам ArrayBuffers и SharedArrayBuffers?
Прямое: ArrayBuffers даёт нам возможность вручную управлять некоторыми данными, даже если мы работаем с JavaScript, который является языком с автоматическим управлением памятью.
А зачем нам это нужно?
Как я уже объяснил в прошлой статье, за автоматическое управление памятью приходится платить. Разработчику работать с ним проще, но оно добавляет лишнюю нагрузку. В некоторых случаях, эта нагрузка может приводить к проблемам с производительностью.

Например, когда мы создаём в JavaScript переменную, движку нужно понять, что это за переменная, и как она должна быть размещена в памяти. Из-за этой неопределённости, движок JavaScript обычно выделяет больше места, чем в действительности под неё требуется. В зависимости от переменной, выделенное место может быть от 2 до 8 раз больше, чем нужно, из-за чего много памяти простаивает впустую.

Кроме того, определённые шаблоны создания и использования объектов JavaScript могут приводить к проблемам со сборкой мусора. Если мы управляем памятью вручную, то можем выбирать ту стратегию распределения ресурсов, которая подходит именно для того вопроса, над которым мы сейчас работаем.

Чаще всего не стоит тратить на это время. В обычных проектах нет таких высоких требований к производительности и нам не нужно заниматься ручным управлением памятью. А в широком ряде случаев она даже замедляет работу.

Но в тех случаях, когда нам приходится работать на низком уровне, чтобы сделать код максимально быстрым, нам на помощь могут прийти ArrayBuffers и SharedArrayBuffers.

Как работает ArrayBuffer?
По сути, работа с ним напоминает работу с любым другим массивом JavaScript. С тем только исключением, что в ArrayBuffer нельзя записывать любые типы данных JavaScript, вроде объектов или текстовых строк. Единственное, что можно туда записывать это байты (которые мы можем задавать, используя числа).

Мне стоит здесь пояснить, что, на самом деле, мы не добавляем этот байт непосредственно в ArrayBuffer. Сам по себе, ArrayBuffer не знает, ни какого размера должен быть байт, ни как конвертировать различные числа в байты.

ArrayBuffer это просто набор нулей и единиц, записанных в одну строку. ArrayBuffer не знает, где должен заканчиваться первый элемент этого массива и начинаться второй.

Чтобы задать окружение, то есть, чтобы разбить эту последовательность на секции, нам нужно поместить её в так называемое «представление». Такие представления данных можно создавать при помощи типизированных массивов, и существует много разных видов типизированных массивов, с которыми они могут работать.

Например, мы можем создать типизированный массив Int8, который сможет разбить последовательность на 8-и битные байты.

Или мы можем создать массив Int16 типа unsigned, который разобьёт её на 16-и битные байты, а также можем обработать её так, как будто бы она является беззнаковым целым.

У нас могут даже быть множественные представления на один и тот же буфер базы данных. Различные представления дадут нам разные результаты одних и тех же действий.

Например, если у нас есть элементы 0 & 1 из Int8 представления этого ArrayBuffer, то они дадут нам иные значения, нежели элемент 0 в представлении Uint16, даже несмотря на то, что они состоят из одних и тех же битов.

Таким образом, ArrayBuffer, по сути, работает как неотформатированная память. Он эмулирует прямой доступ к памяти, который есть в языках типа С.

Вы, наверное, думаете, почему бы тогда нам, вместо создания ещё одного уровня абстракции, не открыть программистам прямой доступ к памяти. Прямой доступ к памяти создал бы дыры в безопасности. Я объясню это подробнее в следующей статье.

А что такое SharedArrayBuffer?
Чтобы объяснить, что такое объект SharedArrayBuffer, мне нужно немного рассказать о параллельном выполнении кода и JavaScript.

Параллельное выполнение кода требуется, чтобы заставить работать его быстрее, а также для того, чтобы он быстрее отвечал на действия пользователей. Для этого нам нужно разделить работу.

В обычном приложении абсолютно всю работу выполняет один-единственный элемент – основной тред. Я уже писал об этом… основной тред похож на специалиста по разработке приложений. Он отвечает и за JavaScript, и за объектную модель, и за макет.

Таким образом, в работе полезно всё, что может сократить нагрузку на основной тред. При определённых обстоятельствах, ArrayBuffers способен сократить объём работы, который он выполняет.

Но бывают ситуации, когда просто облегчение нагрузки на основной тред недостаточно. Иногда нам нужно подкрепление… нам нужно разделить работу.

В большинстве языков программирования работа обычно делится при помощи так называемых «тредов» (потоков выполнения). По сути, это то же самое, что нанять для работы над проектом несколько человек. Если у нас есть невзаимосвязанные задания, мы можем раздать их разным тредам. И тогда оба треда будут работать каждый над своим заданием в одно и то же время.

В JavaScript это делается при помощи так называемого «веб-работника». Эти веб-работники несколько отличаются от тредов, которые используются в других языках. По умолчанию, они не делятся памятью.

То есть, если мы хотим передать какие-то данные другому треду, нам придётся их скопировать. Это делается при помощи функции postMessage.

postMessage принимает любой объект, который мы ей дадим, сериализует его и пересылает другому веб-работнику, где он десериализуется и записывается в память.

Это довольно медленный процесс.

Для некоторых типов данных, таких, как ArrayBuffer, можно делать так называемое «перемещение памяти». То есть, переносить определённый блок памяти таким образом, чтобы у другого веб-работника появлялся к ней доступ.

Но тогда первый веб-работник доступа к ней лишается.

Это работает для некоторых сценариев использования, но в большинстве случаев, если мы хотим организовать высокоэффективную параллельную работу, нам потребуется разделяемая память.

Её нам может обеспечить SharedArrayBuffer.

При помощи SharedArrayBuffer оба веб-работника, оба треда, могут брать информацию из одного и того же отрезка памяти и записывать её туда.

Это значит, что их коммуникация не создаёт лишнюю нагрузку и торможение, которые появляются при использовании postMessage. У обоих веб-работников есть прямой доступ к данным.

Однако прямой доступ к данным для обоих тредов одновременно таит в себе некоторые опасности. Он может привести к так называемым «состояниям гонки».

Я расскажу об этом подробнее в следующей статье.

Какова текущая ситуация с SharedArrayBuffer?
Вскоре SharedArrayBuffer будет поддерживаться всеми основными браузерами.

Он уже встроен в SafariSafari 10.1). Firefox и Chrome начнут поддерживать его с июльских/августовских релизов. А в Edge планируется встроить его во время осеннего апдейта Windows

Даже когда эти объекты появятся во всех основных браузерах, мы не ждём, что разработчики приложений начнут их использовать напрямую. На самом деле, мы это не рекомендуем. Вы должны использовать максимально возможный уровень абстракции.

Но мы ожидаем, что разработчики библиотек JavaScript будут создавать библиотеки, в которых можно будет более просто и безопасно работать с объектами SharedArrayBuffer.

В дополнение, когда объекты SharedArrayBuffer будут встроены в платформу, WebAssembly сможет использовать их для внедрения поддержки тредов. Когда это будет сделано, вы сможете использовать параллельные абстракции в языке вроде Rust, в котором параллельная работа является одной из основных идей.

В следующей статье мы рассмотрим инструменты (атомарные операции), которые разработчикам библиотек стоит использовать для создания этих абстракций, избегая при этом появления состояний гонки.